diff --git a/.vscode/settings.json b/.vscode/settings.json index f43248e..73c8b92 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,20 @@ { + "[powershell][markdown]": { + "editor.colorDecorators": false, + }, "cSpell.words": [ + "Browsable", + "clike", "Cloneable", + "cmatch", + "ilike", + "imatch", + "Linq", "Memberise", - "Memberwise" + "Memberwise", + "notin", + "psobject", + "Soesterberg", + "Steppable" ] } \ No newline at end of file diff --git a/Build/Build-Docs.ps1 b/Build/Build-Docs.ps1 index 8154d9e..544cf74 100644 --- a/Build/Build-Docs.ps1 +++ b/Build/Build-Docs.ps1 @@ -1,5 +1,4 @@ -using module ..\..\ObjectGraphTools -Get-ChildItem $PSScriptRoot\..\Source\Public\*.ps1 | ForEach-Object { +Get-ChildItem $PSScriptRoot\..\Source\Cmdlets\*.ps1 | ForEach-Object { Write-Host $_ - Get-MarkdownHelp $_ | Out-File -Encoding ASCII $PSScriptRoot\..\Docs\$($_.BaseName).md + Get-MarkdownHelp $_ | Out-File $PSScriptRoot\..\Docs\$($_.BaseName).md } \ No newline at end of file diff --git a/Build/Build-Module.ps1 b/Build/Build-Module.ps1 index c3767d0..66c4e68 100644 --- a/Build/Build-Module.ps1 +++ b/Build/Build-Module.ps1 @@ -1,17 +1,24 @@ -<# -.SYNOPSIS - Module Builder - -.DESCRIPTION - Module Builder - +<#PSScriptInfo +.VERSION 0.1.1 +.GUID 19631007-c6ce-4a9f-a32c-dc87fdc8c1ff +.AUTHOR Ronald Bode (iRon) +.DESCRIPTION Build a new module file. +.COMPANYNAME PowerSnippets.com +.COPYRIGHT Ronald Bode (iRon) +.TAGS PowerShell Build Module PSM1 +.LICENSEURI https://github.com/iRon7/Build-Module/LICENSE +.PROJECTURI https://github.com/iRon7/Build-Module +.ICONURI https://raw.githubusercontent.com/iRon7/Build-Module/master/Build-Module.png +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA #> -#Requires -Version 7.4 -#Requires -Modules @{ ModuleName="Microsoft.PowerShell.PSResourceGet"; RequiredVersion="1.0.6" } - using namespace System.Collections using namespace System.Collections.Generic +using namespace System.Collections.Specialized using namespace System.Collections.ObjectModel using namespace System.IO using namespace System.Link @@ -19,38 +26,344 @@ using namespace System.Text using NameSpace System.Management.Automation using NameSpace System.Management.Automation.Language -param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)][String]$SourceFolder, +<# +.SYNOPSIS +Module Builder - [Parameter(Mandatory = $true)][String]$ModulePath, +.DESCRIPTION +Build a new module (`.psm1`) file from a folder containing PowerShell scripts (`.ps1` files) and other resources. + +This module builder doesn't take care of the module manifest (`.psd1` file), but it simply builds the module file +from the scripts and resources in the specified folder while taking of the following: + +* Merging the statements (e.g. `#Requires` and `using` statements) +* Preventing duplicates and collisions (e.g. duplicate function names) +* Ordering the statements based on their dependencies (e.g. classes inheritance) +* Formatting the output. +* Automatically exporting variables, functions, cmdlets, aliases and types. + +It doesn't touch any module settings defined in the module manifest (`.psd1` file), such as the module Version, +NestedModules and ScripsToProcess. The only requirement is that the following settings are **not** defined +(or commented out): + +* ~~FunctionsToExport = @()~~ +* ~~VariablesToExport = @()~~ +* ~~AliasesToExport = @()~~ + +These particular settings are automatically generated based on the cmdlets, variables and aliases defined in the +source scripts and eventually handled by the module (`.psm1`) file. + +The general consensus behind this module builder is that the module author defines the items that should be +**loaded** (imported) by putting them in the module source folder and shouldn't be concerned with *invoking* +(dot-sourcing) any scripts knowing that this could lead to similar concerns as using the [`Invoke-Expression`] +cmdlet especially when working in a team. See also [https://github.com/PowerShell/PowerShell/issues/18740]. + +This means that this module builder will only accept specific statements (blocks) and reject (with a warning) on +statements that require any invocation which potentially could lead to conflicts with other functions and types. + +Anything that concerns a dynamic preparation of the module should be done by a specific module manifest setting +or scripted in the `ScriptsToProcess` setting of the module manifest. + +## Accepted Statements + +The accepted statements might be divided into different files and (sub)folders using any file name or folder name +with the exception of functions that need to be exported as cmdlets. + +The accepted statement types are categorized and loaded in the following order: +* [Requirements] +* [using statements] +* [enum types] +* [Classes] +* [Variables assignments] +* [(private) Functions] +* [(public) Cmdlets] +* [Aliases] +* [Format files] + +### Requirements + +The module builder will merge the `#Requires` statements from the source scripts and will add them to the top of +the module file. + +#### #Required -Version + +If multiple `#required -version` statements are found, the highest version will be used. + +#### #Required -PSEdition + +If conflicting `#required -PSEdition` statements are found, a merge conflict exception is thrown. + +#### #Required -Modules + +If multiple `#required -Modules` statements are found, the module names will be merged and the highest version + +#### #Required -RunAsAdministrator + +If set in any script, the module builder will add the `#Requires -RunAsAdministrator` statement to the top of the +module file. + +> [!TIP] +> Consider to make your function [self-elevating](https://stackoverflow.com/q/60209449/1701026). + +### Using statements + +In general `using` statements are merged and added to the module file except for the `using module` with will be +rejected and a warning will be shown. + +#### using namespace <.NET-namespace> + +The module builder will use the full namespace name and added or merged them accordingly. + +#### using module + +The module builder will reject this statement and will suggest to use the module manifest instead. + +#### using assembly + +The module builder will reformat the assembly path and merge the assembly names and add them to module file. + +### Enum types + +`Enum` and `Flags` types are reformatted using the explicit item value and added or merged them accordingly. + +> [!NOTE] +> All types are [automatically added to the TypeAccelerators list][2] to make sure they are publicly available. + +### Classes + +`Class` definitions are sorted based on any derived (custom) class dependency and added or accordingly. +If conflicting there are multiple classes with the same name a merge conflict exception is thrown unless the +content of the class is exactly the same. + +> [!NOTE] +> All types are [automatically added to the TypeAccelerators list][2] to make sure they are publicly available. + +> [!WARNING] +> PowerShell classes do have some known [limitation][3] and know [issues][4] that might cause problems when using +> a module builder. For example, when dividing classes that are depended of each other over multiple files would +> lead to "*Unable to find type []*" in the "PROBLEMS" tab. The only solution is to put these classes +> in the same file or neglect the specific problem. + +### Variables assignments + +The module builder will merge the variable assignments and add export them when the module is loaded. + +> [!TIP] +> For variables that are dynamically assigned during module load time, consider to use the `ScriptsToProcess` +> setting in the module manifest instead or define the variable during the concerned function or class execution. + +### (Private) Functions + +Any function that is defined in the source scripts will be added to the module file as a private function. +Meaning the function will not be exported by the module builder and will not be available to the user +when the module is loaded. The function will only be available to other functions in the module file. +To export a function as a cmdlet, the function needs to be defined in a script file with the `.ps1` extension, +see: [(public) cmdlets]. + +### (Public) cmdlets + +Any (public) function that needs to be exported by the module builder is called a [cmdlets][5] in this design. +The module builder will recognize any PowerShell script file (`.ps1`) that contains a `param` block and will treat +it as a cmdlet. The name of the script file will be used as the cmdlet name. Any `Required` or `Using` statement +will be merged and added to the module file. + +This module builder design enforces the use of advanced functions and prevents coincidentally interfering with +other cmdlets or other items in the module framework. See also [Add `ScriptsToInclude` to the Module Manifest][4]. + +#### Cmdlet Prototype Pester testing + +This concept facilitates troubleshooting and testing prototypes without (re)building a new module: + + #Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"} + + using module MyModule - [Int]$Depth = 1, + param([alias("Path")]$PrototypePath) - [Switch]$KeepExistingSettings + Describe 'Test-Object' { + + BeforeAll { + + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + } + + ... + +Testing a prototype: + + . Tests\MyCmdlet.Tests.ps1 Source\Cmdlets\MyCmdlet.ps1 + + +### Aliases + +This module builder only supports aliases for (public) cmdlets (exported functions). A cmdlet alias might be set +using the [Alias Attribute Declaration][6], this will export the alias when the module is loaded. + +> [!NOTE] +> The [Set-Alias] command statement is rejected as aliases should be avoided for private functions as they can +> make code difficult to read, understand and impact availability (see: [AvoidUsingCmdletAliases][7]). + +### Format files + +The module builder will accept PowerShell format files (`.ps1xml`) and will merge the view definitions. +For more details on formatting views, see: [about Types.ps1xml][8]. + +## Rejected Statements + +In general, any statement that requires any invocation (dot-sourcing) is rejected by the module builder. +This includes any native cmdlet (and is not limited to) the following specific statements: + +### Install-Module + +The [Install-Module] cmdlet is rejected along with other cmdlet commands, to specify scripts that run in the +module's session state, use the `NestedModules` manifest setting. + +### Install-Module + +The [New-Type] cmdlet is rejected along with other cmdlet commands, to load any assembly or type definitions +written in a different language than PowerShell, use the `RequiredAssemblies` manifest setting or the +`ScriptsToProcess` manifest setting for any dynamic or conditional loading. Or consider to load the required +type just-in-time while executing the depended class or cmdlet. + +.EXAMPLE +# (Re)build a new module file + +Build a new module file from the scripts in the `.\Scripts` folder and save it to `.\MyModule.psm1`. + + Build-Module -SourceFolder .\Scripts -ModulePath .\MyModule.psm1 + +.PARAMETERS SourceFolder + +The source folder containing the PowerShell scripts and resources to build the module from. + +.PARAMETERS ModulePath + +The path to the module file to create. + +> [!WARNING] +> The module file will be overwritten if it already exists. + +.PARAMETERS Depth + +The depth of the source folder structure to search for scripts and resources. Default is `1`. + +.LINK +[1]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/cmdlet-overview "cmdlet overview" +[2]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#exporting-classes-with-type-accelerators "Exporting classes with type accelerators" +[3]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#limitations "Class limitations" +[4]: https://github.com/PowerShell/PowerShell/issues/6652 "Various PowerShell Class Issues" +[5]: https://github.com/PowerShell/PowerShell/issues/24253 "Add ScriptsToInclude to the Module Manifest" +[6]: https://learn.microsoft.com/powershell/scripting/developer/cmdlet/alias-attribute-declaration "Alias Attribute Declaration" +[7]: https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/avoidusingcmdletaliases "Avoid using cmdlet aliases" +[8]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_types.ps1xml "about Types.ps1xml" +#> + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'function', Target = '')] +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '', Scope = 'function', Target = '')] +param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)][String]$SourceFolder, + [Parameter(Mandatory = $true)][String]$ModulePath, + [Int]$Depth = 1 ) Begin { + $ErrorActionPreference = "Stop" $Script:SourcePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SourceFolder) - function Use-Script([Alias('Name')][String]$ScriptName, [Alias('Version')][Version]$ScriptVersion) { - $Command = Get-Command $ScriptName -ErrorAction SilentlyContinue - if ( - -not $Command -and - -not ($ScriptVersion -and (Get-PSScriptFileInfo $Command.Source).Version -lt $ScriptVersion) -and - -not (Install-Script $ScriptName -MinimumVersion $ScriptVersion -PassThru) - ) { - $MissingVersion = if ($ScriptVersion) { " version $ScriptVersion" } - $ErrorRecord = [ErrorRecord]::new( - "Missing command: '$ScriptName'$MissingVersion.", - 'MissingScript', 'InvalidArgument', $ScriptName - ) - $PSCmdlet.ThrowTerminatingError($ErrorRecord) + function Sort-Topological { # https://github.com/iRon7/Sort-Topological + [CmdletBinding()][OutputType([List[Object]])]Param( + [Parameter(ValueFromPipeline = $True, Mandatory = $True)]$InputObject, + [Parameter(Position = 0, Mandatory = $True)][Alias('DependencyName')]$EdgeName, + [Parameter(Position = 1)][Alias('NameId')][String]$IdName + ) + + begin { + function Throw-Error($ErrorRecord) { $PSCmdlet.ThrowTerminatingError($ErrorRecord) } + + Function FormatId ($Vertex) { + if ($Vertex -is [ValueType] -or $Vertex -is [String]) { $Value = $Vertex } + elseif (@($_.PSObject.Properties.Name).Contains($IdName)) { $Value = $_.PSObject.Properties[$IdName].Value } + else { return "[$(@($List).IndexOf($Vertex))]" } + if ($Value -is [String]) { """$Value""" } else { $Value } + } + + if ($EdgeName -is [ScriptBlock]) { # Prevent code injection + $Ast = [System.Management.Automation.Language.Parser]::ParseInput($EdgeName, [ref]$null, [ref]$null) + $Expression = $Ast.EndBlock.Statements.PipelineElements.Expression + While ($Expression -is [MemberExpressionAst] -and $Expression.Member -is [StringConstantExpressionAst]) { + $Expression = $Expression.Expression + } + if ($Expression -isnot [VariableExpressionAst] -or $Expression.VariablePath.UserPath -notin '_', 'PSItem') { + $Message = "The { $Expression } expression should contain safe path." + Throw-Error ([ErrorRecord]::new($Message, 'InvalidIdExpression', 'InvalidArgument', $Expression)) + } + } + elseif ($Null -ne $IdName -and $IdName -isnot [String]) { $IdName = "$IdName" } } - } - Use-Script -Name Use-ClassAccessors - Use-Script -Name Sort-Topological -Version 0.1.2 + Process { + $ById = $Null + $Sorted = [List[Object]]::new() + if ($Input) { $List = $Input } else { $List = $InputObject } + if ($List -isnot [iEnumerable]) { return $List } + $EdgeCount = 0 + while ($Sorted.get_Count() -lt $List.get_Count()) { + $Stack = [Stack]::new() + $Enumerator = $List.GetEnumerator() + while ($Enumerator.MoveNext()) { + $Vertex = $Enumerator.Current + if($Sorted.Contains($Vertex)) { continue } + $Edges = [List[Object]]::new() + if ($EdgeName -is [ScriptBlock]) { $Edges = $Vertex.foreach($EdgeName).where{ $Null -ne $_ } } + else { $Edges = $Vertex.PSObject.Properties[$EdgeName].Value } + if ($Null -eq $Edges) { $Edges = @() } elseif ($Edges -isnot [iList]) { $Edges = @($Edges) } + if ($Null -eq $ById -and $Edges.Count -gt 0) { + if ($Edges[0] -is [ValueType] -or $Edges[0] -is [String]) { + if (-not $IdName) { + $Message = 'Dependencies by id require the IdName parameter.' + Throw-Error ([ErrorRecord]::new($Message, 'MissingIdName', 'InvalidArgument', $Vertex)) + } + $ById = @{} + foreach ($Item in $List) { $ById[$Item.PSObject.Properties[$IdName].Value] = $Item } + } else { $ById = $False } + } + if ($ById) { + $Ids = $Edges; $Edges = [List[Object]]::new() + foreach ($Id in $Ids) { + if ($Null -eq $Id) { } elseif ($ById.contains($Id)) { $Edges.Add($ById[$Id]) } + else { + $Message = "Unknown vertex id: $(FormatId $Id)." + Write-Error ([ErrorRecord]::new($Message, 'UnknownVertex', 'InvalidArgument', $Vertex)) + } + + } + } + if ($Stack.Count -gt 0 -or $Edges.Count -eq $EdgeCount) { + $At = if ($Stack.Count -gt 0) { @($Stack.Current).IndexOf($Vertex) + 1 } + $Stack.Push($Enumerator) + if ($At -gt 0) { + $Message = "Circular dependency: $((@($Stack)[0..$At].Current).foreach{ FormatId $_ } -Join ', ')." + Throw-Error ([ErrorRecord]::new($Message, 'CircularDependency', 'InvalidArgument', $Vertex)) + } + $Enumerator = $Edges.GetEnumerator() + } + } + if ($Stack.Count -gt 0) { + $Enumerator = $Stack.Pop() + $Vertex = $Enumerator.Current + if ($Vertex -is [ValueType] -or $Vertex -is [String]) { $Vertex = $ById[$Vertex] } + if (-not $Sorted.Contains($Vertex)) { $Sorted.Add($Vertex) } + } + else { $EdgeCount++ } + } + $Sorted + } + } function New-LocationMessage([String]$Message, [String]$FilePath, $Target) { if ($Message -like '*.' -and $Message -notlike '*..') { $Message = $Message.Remove($Message.Length - 1) } @@ -75,7 +388,7 @@ Begin { $Id = if ($ErrorRecord -is [ErrorRecord]) { $ErrorRecord.FullyQualifiedErrorId } else { 'ModuleBuildError' } $Category = if ($ErrorRecord -is [ErrorRecord]) { $ErrorRecord.CategoryInfo.Category } else { 'ParserError' } - $Message = New-LocationMessage $ErrorRecord $FilePath $Extent + $Message = New-LocationMessage -Message $ErrorRecord -FilePath $FilePath -Target $Extent [ErrorRecord]::new($Message, $Id, $Category, $Module) } @@ -98,11 +411,9 @@ Begin { class Omission: Exception { Omission([string]$Message): base ($Message) {} } class ModuleRequirements { - static ModuleRequirements() { Use-ClassAccessors } - [Version]$Version [String]$PSEdition - [Ordered]$Modules = @{} + [OrderedDictionary]$Modules = [OrderedDictionary]::new([StringComparer]::InvariantCultureIgnoreCase) [Bool]$RunAsAdministrator hidden [String[]]get_Values() { @@ -188,8 +499,6 @@ Begin { } class ModuleUsingStatements { - static ModuleUsingStatements() { Use-ClassAccessors } - [HashSet[String]]$Namespace = [HashSet[String]]::new([StringComparer]::InvariantCultureIgnoreCase) [HashSet[String]]$Assembly = [HashSet[String]]::new([StringComparer]::InvariantCultureIgnoreCase) @@ -221,10 +530,9 @@ Begin { class ModuleBuilder { static [String]$Tab = ' ' # Used for indenting cmdlet contents - static ModuleBuilder() { Use-ClassAccessors } # Doesn't work with Pester (and classes in process blocks?) [string] $Path - [String] get_Name() { return [Path]::GetFileNameWithoutExtension($this.Path) } + [String] $Name ModuleBuilder($Path) { $FullPath = [Path]::GetFullPath($Path) @@ -234,15 +542,19 @@ Begin { $this.Path = [Path]::Combine($FullPath, "$([Path]::GetFileName($Path)).psm1") } else { Throw "The module path '$Path' is not a folder or doesn't have a '.psm1' extension." } + $this.Name = [Path]::GetFileNameWithoutExtension($this.Path) } [String]GetRelativePath([String]$Path) { - $RelativePath = Resolve-Path -Path $Path -RelativeBasePath ([Path]::GetDirectoryName($this.Path)) -Relative - if ($RelativePath.StartsWith('.\')) { $RelativePath = $RelativePath.SubString(2) } + $ToPath = $Path -split '[\\\/]' + $BasePath = [Path]::GetDirectoryName($this.Path) -split '[\\\/]' + for ($i = 0; $i -lt $BasePath.Length; $i++) { if ($ToPath[$i] -ne $BasePath[$i]) { break } } + $RelativePath = '..\' * ($BasePath.Length - $i) + $RelativePath += $ToPath[$i..($ToPath.Length - 1)] -join [IO.Path]::DirectorySeparatorChar return $RelativePath } - hidden [Ordered]$Sections = [Ordered]@{} + hidden [OrderedDictionary]$Sections = [OrderedDictionary]::new([StringComparer]::InvariantCultureIgnoreCase) AddRequirement([ScriptRequirements]$Requires) { if (-not $this.Sections['Requires']) { $this.Sections['Requires'] = [ModuleRequirements]::new() } @@ -255,7 +567,9 @@ Begin { } } hidden AddStatement([String]$SectionName, [String]$StatementId, $Definition) { - if (-not $this.Sections[$SectionName]) { $this.Sections[$SectionName] = [Ordered]@{} } + if (-not $this.Sections[$SectionName]) { + $this.Sections[$SectionName] = [OrderedDictionary]::new([StringComparer]::InvariantCultureIgnoreCase) + } try { $this.CheckDuplicate($SectionName, $StatementId, $Definition) } catch { throw } $this.Sections[$SectionName][$StatementId] = $Definition } @@ -287,10 +601,10 @@ Begin { else { throw [Omission]"Rejected type (use manifest instead)." } } AssignmentStatementAst { - $Name = $Statement.Left.VariablePath.UserPath + $VariableName = $Statement.Left.VariablePath.UserPath $Expression = $Statement.Right.Extent.Text - if ($Name -eq 'Null' ) { throw [Omission]'Rejected assignment to $Null.' } - try { $this.AddStatement('Variable', $Name, $Expression) } catch { throw } + if ($VariableName -eq 'Null' ) { throw [Omission]'Rejected assignment to $Null.' } + try { $this.AddStatement('Variable', $VariableName, $Expression) } catch { throw } } FunctionDefinitionAst { try { $this.AddStatement('Function', $Statement.Name, $Statement) } catch { throw } @@ -325,7 +639,9 @@ Begin { if ($Token.Type -eq 'String') { $this.AddStatement('Alias', $Token.Content, $Name) $AliasExists = Get-Alias $Token.Content -ErrorAction SilentlyContinue - if ($AliasExists) { Write-Warning "The alias '$($Token.Content)' ($($AliasExists.ResolvedCommand)) already exists." } + if ($AliasExists -and $AliasExists.Source -ne $this.Name) { + Write-Warning "The alias '$($Token.Content)' ($($AliasExists.ResolvedCommand)) already exists." + } } elseif ($Token.Type -eq 'Operator' -and $Token.Content -eq ',') { <# continue #> } elseif ($Token.Type -eq 'GroupEnd') { $AliasGroupToken = $null } @@ -346,7 +662,9 @@ Begin { } AddFormat($SourceFile) { $RelativePath = $this.GetRelativePath($SourceFile) - if (-not $this.Sections['Format']) { $this.Sections['Format'] = [Ordered]@{} } + if (-not $this.Sections['Format']) { + $this.Sections['Format'] = [OrderedDictionary]::new([StringComparer]::InvariantCultureIgnoreCase) + } $Xml = [xml](get-Content $SourceFile) foreach ($Name in $Xml.Configuration.ViewDefinitions.View.Name) { if ($this.Sections['Format'].Contains($Name)) { throw [Collision]"Merge conflict with format '$Name'" } @@ -403,13 +721,13 @@ Begin { if (-not $Aliases.ContainsKey($Name)) { $Aliases[$Name] = [List[String]]::new() } $Aliases[$Name].Add($S.Alias[$Name]) } - $Statements = foreach ($Name in $Aliases.Keys) { "Set-Alias -Name '$($Aliases[$Name])' -Value '$Name'" } + $Statements = foreach ($Name in $Aliases.Keys) { "Set-Alias -Name '$Name' -Value '$($Aliases[$Name])'" } $this.AppendRegion('Alias', $Statements) } if ($S.Contains('Format')) { # https://github.com/PowerShell/PowerShell/issues/17345 # if (-not (Get-FormatData -ErrorAction Ignore $etsTypeName)) { # See: https://stackoverflow.com/a/67991167/1701026 - $Files = [Ordered]@{} + $Files = [OrderedDictionary]::new([StringComparer]::InvariantCultureIgnoreCase) foreach ($Name in $S.Format.get_Keys()) { $FileName = $S.Format[$Name] if (-not $S.Format.Contains($FileName)) { $Files[$FileName] = [List[String]]::new() } @@ -481,15 +799,15 @@ $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { } } - function Select-Statements($Statements, $SourceFile) { + function Select-Statement($Statements, $SourceFile) { if (-Not $Statements) { return } foreach ($Statement in $Statements) { try { if ($Statement -is [ScriptRequirements]) { $Module.AddRequirement($Statement) } else { $Module.AddStatement($Statement) } } - catch [Collision] { $PSCmdlet.ThrowTerminatingError((New-ModuleError $_ $Module $SourceFile $Statement)) } - catch [Omission] { New-LocationMessage $_ $SourceFile $Statement | Write-Warning } + catch [Collision] { $PSCmdlet.ThrowTerminatingError((New-ModuleError -ErrorRecord $_ -Module $Module -FilePath $SourceFile -Extent $Statement)) } + catch [Omission] { New-LocationMessage -Message $_ -FilePath $SourceFile -Target $Statement | Write-Warning } } } @@ -508,10 +826,10 @@ process { .ps1 { $Content = Get-Content -Raw $SourceFile.FullName $Ast = [Parser]::ParseInput($Content, [ref]$Null, [ref]$Null) - Select-Statements $Ast.ScriptRequirements $RelativePath - Select-Statements $Ast.UsingStatements $RelativePath + Select-Statement $Ast.ScriptRequirements $RelativePath + Select-Statement $Ast.UsingStatements $RelativePath if ($Ast.ParamBlock) { $Module.AddCmdlet($SourceFile.BaseName, $Content) } - else { Select-Statements $Ast.EndBlock.Statements $RelativePath } + else { Select-Statement $Ast.EndBlock.Statements $RelativePath } } .ps1xml { $Module.AddFormat($SourceFile) diff --git a/Build/Build-ObjectGraphTools.ps1 b/Build/Build-ObjectGraphTools.ps1 index d0857d6..4fbbf90 100644 --- a/Build/Build-ObjectGraphTools.ps1 +++ b/Build/Build-ObjectGraphTools.ps1 @@ -1,3 +1,5 @@ +# . .\Build\Build-Module.ps1 -ModulePath .\ObjectGraphTools.psm1 -SourceFolder .\Source + $Params = @{ ModulePath = "$PSScriptRoot\..\ObjectGraphTools.psm1" SourceFolder = "$PSScriptRoot\..\Source" diff --git a/Docs/Compare-ObjectGraph.md b/Docs/Compare-ObjectGraph.md index 219ea58..9c40686 100644 --- a/Docs/Compare-ObjectGraph.md +++ b/Docs/Compare-ObjectGraph.md @@ -25,9 +25,9 @@ Deep compares two Object Graph and lists the differences between them. ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> -The input object that will be compared with the reference object (see: [-Reference](#-reference) parameter). +The input object that will be compared with the reference object (see: [`-Reference`](#-reference) parameter). > [!NOTE] > Multiple input object might be provided via the pipeline. @@ -38,57 +38,69 @@ The input object that will be compared with the reference object (see: [-Referen ,$InputObject | Compare-ObjectGraph $Reference. ``` - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Reference `** +### `-Reference` <Object> -The reference that is used to compared with the input object (see: [-InputObject](#-inputobject) parameter). +The reference that is used to compared with the input object (see: [`-InputObject`](#-inputobject) parameter). - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Reference +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-PrimaryKey `** +### `-PrimaryKey` <String[]> If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched based on the values of the `-PrimaryKey` supplied. - - - - - - - -
Type:String[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -PrimaryKey +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-IsEqual`** +### `-IsEqual` If set, the cmdlet will return a boolean (`$true` or `$false`). As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -IsEqual +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MatchCase`** +### `-MatchCase` Unless the `-MatchCase` switch is provided, string values are considered case insensitive. @@ -97,16 +109,19 @@ Unless the `-MatchCase` switch is provided, string values are considered case in > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison > is case insensitive otherwise the comparer supplied with the dictionary is used. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MatchCase +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MatchType`** +### `-MatchType` Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the `$Reference` object is leading. Meaning `$Reference -eq $InputObject`: @@ -116,27 +131,33 @@ Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) ``` - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-IgnoreListOrder`** +```powershell +Name: -MatchType +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+### `-IgnoreListOrder` + +```powershell +Name: -IgnoreListOrder +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MatchMapOrder`** +### `-MatchMapOrder` By default, items in dictionary (including properties of an PSCustomObject or Component Object) are matched by their key name (independent of the order). @@ -146,26 +167,32 @@ If the `-MatchMapOrder` switch is supplied, each entry is also validated by the > A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MatchMapOrder +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> The maximal depth to recursively compare each embedded property (default: 10). - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) diff --git a/Docs/ConvertFrom-Expression.md b/Docs/ConvertFrom-Expression.md index eceb7d0..b376ad2 100644 --- a/Docs/ConvertFrom-Expression.md +++ b/Docs/ConvertFrom-Expression.md @@ -17,11 +17,11 @@ ConvertFrom-Expression ## Description The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph -existing of a mixture of nested arrays, hashtables and objects that contain a list of strings and values. +existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. ## Parameters -### **`-InputObject `** +### `-InputObject` <String> Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. @@ -29,21 +29,28 @@ or type a command or expression that gets the string. You can also pipe a string The **InputObject** parameter is required, but its value can be an empty string. The **InputObject** value can't be `$null` or an empty string. - - - - - - - -
Type:String
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: -Expression +Type: [String] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LanguageMode `** +### `-LanguageMode` <PSLanguageMode> Defines which object types are allowed for the deserialization, see: [About language modes][2] * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + +```PowerShell `[String]`, `[Array]` or `[HashTable]`. +``` + * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. > [!Caution] @@ -56,41 +63,50 @@ Defines which object types are allowed for the deserialization, see: [About lang > best to design your configuration expressions with restricted or constrained classes, rather than > allowing full freeform expressions. - - - - - - - -
Type:PSLanguageMode
Mandatory:False
Position:Named
Default value:'Restricted'
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LanguageMode +Aliases: # None +Type: [PSLanguageMode] +Value (default): 'Restricted' +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ListAs `** +### `-ListAs` <Object> If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or denied type initializer will be converted to the given list type. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ListAs +Aliases: -ArrayAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MapAs `** +### `-MapAs` <Object> If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown or denied type initializer will be converted to the given map (dictionary or object) type. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MapAs +Aliases: -DictionaryAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) diff --git a/Docs/ConvertTo-Expression.md b/Docs/ConvertTo-Expression.md index 4781e86..b079c1a 100644 --- a/Docs/ConvertTo-Expression.md +++ b/Docs/ConvertTo-Expression.md @@ -41,22 +41,25 @@ $Object = Invoke-Expression ($Object | ConvertTo-Expression) ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, or type a command or expression that gets the objects. You can also pipe one or more objects to `ConvertTo-Expression.` - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LanguageMode `** +### `-LanguageMode` <PSLanguageMode> Defines which object types are allowed for the serialization, see: [About language modes][2] If a specific type isn't allowed in the given language mode, it will be substituted by: @@ -71,16 +74,19 @@ If a specific type isn't allowed in the given language mode, it will be substitu See the [PSNode Object Parser][1] for a detailed definition on node types. - - - - - - - -
Type:PSLanguageMode
Mandatory:False
Position:Named
Default value:'Restricted'
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LanguageMode +Aliases: # None +Type: [PSLanguageMode] +Value (default): 'Restricted' +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ExpandDepth `** +### `-ExpandDepth` <Int32> Defines up till what level the collections will be expanded in the output. @@ -91,16 +97,19 @@ Defines up till what level the collections will be expanded in the output. > White spaces (as newline characters and spaces) will not be removed from the content > of a (here) string. - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[Int]::MaxValue
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ExpandDepth +Aliases: -Expand +Type: [Int32] +Value (default): [Int]::MaxValue +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Explicit`** +### `-Explicit` By default, restricted language types initializers are suppressed. When the `Explicit` switch is set, *all* values will be prefixed with an initializer @@ -109,33 +118,39 @@ When the `Explicit` switch is set, *all* values will be prefixed with an initial > [!Note] > The `-Explicit` switch can not be used in **restricted** language mode - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Explicit +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-FullTypeName`** +### `-FullTypeName` In case a value is prefixed with an initializer, the full type name of the initializer is used. > [!Note] > The `-FullTypename` switch can not be used in **restricted** language mode and will only be -> meaningful if the initializer is used (see also the [-Explicit](#-explicit) switch). - - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+> meaningful if the initializer is used (see also the [`-Explicit`](#-explicit) switch). + +```powershell +Name: -FullTypeName +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-HighFidelity`** +### `-HighFidelity` If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. @@ -158,53 +173,65 @@ due to constructor limitations such as readonly property. > [!Note] > The Object property `TypeId = []` is always excluded. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -HighFidelity +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ExpandSingleton`** +### `-ExpandSingleton` (List or map) collections nodes that contain a single item will not be expanded unless this `-ExpandSingleton` is supplied. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-Indent `** +```powershell +Name: -ExpandSingleton +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` - - - - - - - -
Type:String
Mandatory:False
Position:Named
Default value:' '
Accept pipeline input:False
Accept wildcard characters:False
+### `-Indent` <String> + +```powershell +Name: -Indent +Aliases: # None +Type: [String] +Value (default): ' ' +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> Specifies how many levels of contained objects are included in the PowerShell representation. The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Inputs @@ -217,8 +244,10 @@ String[]. `ConvertTo-Expression` returns a PowerShell [String](#string) expressi ## Related Links -* 1: [PowerShell Object Parser][1] -* 2: [About language modes][2] +* [PowerShell Object Parser](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md) +* [About language modes](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" diff --git a/Docs/Copy-ObjectGraph.md b/Docs/Copy-ObjectGraph.md index 9b58a54..fa50a4d 100644 --- a/Docs/Copy-ObjectGraph.md +++ b/Docs/Copy-ObjectGraph.md @@ -21,21 +21,21 @@ Recursively ("deep") copies a object graph. ## Examples -### Example 1: Deep copy a complete object graph into a new object graph +### Example 1: Deep copy a complete object graph into a new object graph ```PowerShell $NewObjectGraph = Copy-ObjectGraph $ObjectGraph ``` -### Example 2: Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects +### Example 2: Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects ```PowerShell $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject ``` -### Example 3: Convert a Json string to an object graph with (case insensitive) ordered dictionaries +### Example 3: Convert a Json string to an object graph with (case insensitive) ordered dictionaries ```PowerShell @@ -44,72 +44,89 @@ $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered] ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> The input object that will be recursively copied. - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ListAs `** +### `-ListAs` <Object> If supplied, lists will be converted to the given type (or type of the supplied object example). - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-MapAs `** +```powershell +Name: -ListAs +Aliases: -ArrayAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+### `-MapAs` <Object> + +```powershell +Name: -MapAs +Aliases: -DictionaryAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ExcludeLeafs`** +### `-ExcludeLeafs` If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. If omitted, each leaf will be shallow copied - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-MaxDepth `** - - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ExcludeLeafs +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` + +### `-MaxDepth` <Int32> + +```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [PSCustomObject Class][1] -* 2: [Component Class][2] +* [PSCustomObject Class](https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject) +* [Component Class](https://learn.microsoft.com/dotnet/api/system.componentmodel.component) + + [1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" [2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" diff --git a/Docs/Export-ObjectGraph.md b/Docs/Export-ObjectGraph.md index 6ab70d5..6d5ad0d 100644 --- a/Docs/Export-ObjectGraph.md +++ b/Docs/Export-ObjectGraph.md @@ -7,6 +7,7 @@ Serializes a PowerShell File or object-graph and exports it to a PowerShell (dat ```PowerShell Export-ObjectGraph + -Path -InputObject [-LanguageMode ] [-ExpandDepth = [Int]::MaxValue] @@ -20,15 +21,19 @@ Export-ObjectGraph [] ``` -```PowerShell -Export-ObjectGraph - -Path - [] -``` - ```PowerShell Export-ObjectGraph -LiteralPath + -InputObject + [-LanguageMode ] + [-ExpandDepth = [Int]::MaxValue] + [-Explicit] + [-FullTypeName] + [-HighFidelity] + [-ExpandSingleton] + [-Indent = ' '] + [-MaxDepth = [PSNode]::DefaultMaxDepth] + [-Encoding ] [] ``` @@ -39,48 +44,57 @@ and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file ## Parameters -### **`-InputObject `** - - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+### `-InputObject` <Object> + +```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Path `** +### `-Path` <String[]> Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. Wildcard characters are permitted. - - - - - - - -
Type:String[]
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Path +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: Path +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LiteralPath `** +### `-LiteralPath` <String[]> Specifies a path to one or more locations where PowerShell should export the object-graph. The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell PowerShell not to interpret any characters as escape sequences. - - - - - - - -
Type:String[]
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LiteralPath +Aliases: -PSPath, -LP +Type: [String[]] +Value (default): # Undefined +Parameter sets: LiteralPath +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LanguageMode `** +### `-LanguageMode` <PSLanguageMode> Defines which object types are allowed for the serialization, see: [About language modes][2] If a specific type isn't allowed in the given language mode, it will be substituted by: @@ -95,16 +109,19 @@ If a specific type isn't allowed in the given language mode, it will be substitu See the [PSNode Object Parser][1] for a detailed definition on node types. - - - - - - - -
Type:PSLanguageMode
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LanguageMode +Aliases: # None +Type: [PSLanguageMode] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ExpandDepth `** +### `-ExpandDepth` <Int32> Defines up till what level the collections will be expanded in the output. @@ -115,16 +132,19 @@ Defines up till what level the collections will be expanded in the output. > White spaces (as newline characters and spaces) will not be removed from the content > of a (here) string. - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[Int]::MaxValue
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ExpandDepth +Aliases: -Expand +Type: [Int32] +Value (default): [Int]::MaxValue +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Explicit`** +### `-Explicit` By default, restricted language types initializers are suppressed. When the `Explicit` switch is set, *all* values will be prefixed with an initializer @@ -133,33 +153,39 @@ When the `Explicit` switch is set, *all* values will be prefixed with an initial > [!Note] > The `-Explicit` switch can not be used in **restricted** language mode - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Explicit +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-FullTypeName`** +### `-FullTypeName` In case a value is prefixed with an initializer, the full type name of the initializer is used. > [!Note] > The `-FullTypename` switch can not be used in **restricted** language mode and will only be -> meaningful if the initializer is used (see also the [-Explicit](#-explicit) switch). - - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+> meaningful if the initializer is used (see also the [`-Explicit`](#-explicit) switch). + +```powershell +Name: -FullTypeName +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-HighFidelity`** +### `-HighFidelity` If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. @@ -182,71 +208,88 @@ due to constructor limitations such as readonly property. > [!Note] > Objects properties of type `[Reflection.MemberInfo]` are always excluded. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -HighFidelity +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ExpandSingleton`** +### `-ExpandSingleton` (List or map) collections nodes that contain a single item will not be expanded unless this `-ExpandSingleton` is supplied. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-Indent `** +```powershell +Name: -ExpandSingleton +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` - - - - - - - -
Type:String
Mandatory:False
Position:Named
Default value:' '
Accept pipeline input:False
Accept wildcard characters:False
+### `-Indent` <String> + +```powershell +Name: -Indent +Aliases: # None +Type: [String] +Value (default): ' ' +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> Specifies how many levels of contained objects are included in the PowerShell representation. -The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). - - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). + +```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Encoding `** +### `-Encoding` <Object> Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Encoding +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [PowerShell Object Parser][1] -* 2: [About language modes][2] +* [PowerShell Object Parser](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md) +* [About language modes](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" diff --git a/Docs/Get-ChildNode.md b/Docs/Get-ChildNode.md index 69e2a7b..d7abdb0 100644 --- a/Docs/Get-ChildNode.md +++ b/Docs/Get-ChildNode.md @@ -7,6 +7,7 @@ Gets the child nodes of an object-graph ```PowerShell Get-ChildNode + [-ListChild] -InputObject [-Recurse] [-AtDepth ] @@ -17,17 +18,18 @@ Get-ChildNode [] ``` -```PowerShell -Get-ChildNode - [-ListChild] - [] -``` - ```PowerShell Get-ChildNode [-Include ] [-Exclude ] [-Literal] + -InputObject + [-Recurse] + [-AtDepth ] + [-Leaf] + [-IncludeSelf] + [-ValueOnly] + [-MaxDepth ] [] ``` @@ -38,7 +40,7 @@ The returned nodes are unique even if the provide list of input parent nodes hav ## Examples -### Example 1: Select all leaf nodes in a object graph +### Example 1: Select all leaf nodes in a object graph Given the following object graph: @@ -85,7 +87,7 @@ Path Name Depth Value .Comment Comment 1 Sample ObjectGraph ``` -### Example 2: update a property +### Example 2: update a property The following example selects all child nodes named `Comment` at a depth of `3`. @@ -124,20 +126,23 @@ See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties a ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> The concerned object graph or node. - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Recurse`** +### `-Recurse` Recursively iterates through all embedded property objects (nodes) to get the selected nodes. The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` @@ -153,135 +158,162 @@ Get-Node -Depth 20 | Get-ChildNode ... > If the [AtDepth](#atdepth) parameter is supplied, the object graph is recursively searched anyways > for the selected nodes up till the deepest given `AtDepth` value. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Recurse +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-AtDepth `** +### `-AtDepth` <Int32[]> When defined, only returns nodes at the given depth(s). > [!NOTE] > The nodes below the `MaxDepth` can not be retrieved. - - - - - - - -
Type:Int32[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -AtDepth +Aliases: # None +Type: [Int32[]] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ListChild`** +### `-ListChild` Returns the closest nodes derived from a **list node**. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ListChild +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: ListChild +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Include `** +### `-Include` <String[]> Returns only nodes derived from a **map node** including only the ones specified by one or more string patterns defined by this parameter. Wildcard characters are permitted. > [!NOTE] -> The [-Include](#-include) and [-Exclude](#-exclude) parameters can be used together. However, the exclusions are applied +> The [`-Include`](#-include) and [`-Exclude`](#-exclude) parameters can be used together. However, the exclusions are applied > after the inclusions, which can affect the final output. - - - - - - - -
Type:String[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Include +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: MapChild +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Exclude `** +### `-Exclude` <String[]> Returns only nodes derived from a **map node** excluding the ones specified by one or more string patterns defined by this parameter. Wildcard characters are permitted. > [!NOTE] -> The [-Include](#-include) and [-Exclude](#-exclude) parameters can be used together. However, the exclusions are applied +> The [`-Include`](#-include) and [`-Exclude`](#-exclude) parameters can be used together. However, the exclusions are applied > after the inclusions, which can affect the final output. - - - - - - - -
Type:String[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Exclude +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: MapChild +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Literal`** +### `-Literal` -The values of the [-Include](#-include) - and [-Exclude](#-exclude) parameters are used exactly as it is typed. +The values of the [`-Include`](#-include) - and [`-Exclude`](#-exclude) parameters are used exactly as it is typed. No characters are interpreted as wildcards. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Literal +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: MapChild +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Leaf`** +### `-Leaf` Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. -You can use the [-Recurse](#-recurse) parameter with the [-Leaf](#-leaf) parameter. - - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+You can use the [`-Recurse`](#-recurse) parameter with the [`-Leaf`](#-leaf) parameter. + +```powershell +Name: -Leaf +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-IncludeSelf`** +### `-IncludeSelf` Includes the current node with the returned child nodes. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -IncludeSelf +Aliases: -Self +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ValueOnly`** +### `-ValueOnly` returns the value of the node instead of the node itself. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ValueOnly +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. The failsafe will prevent infinitive loops for circular references as e.g. in: @@ -297,19 +329,24 @@ The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: # None +Type: [Int32] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [PowerShell Object Parser][1] -* 2: [Get-Node][2] +* [PowerShell Object Parser](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md) +* [Get-Node](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" diff --git a/Docs/Get-Node.md b/Docs/Get-Node.md index f34f891..94089f4 100644 --- a/Docs/Get-Node.md +++ b/Docs/Get-Node.md @@ -7,26 +7,22 @@ Get a node ```PowerShell Get-Node + [-Path ] + [-Literal] -InputObject + [-ValueOnly] [-Unique] [-MaxDepth ] [] ``` -```PowerShell -Get-Node - [-Path ] - [-Literal] - [] -``` - ## Description The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. ## Examples -### Example 1: Parse a object graph to a node instance +### Example 1: Parse a object graph to a node instance The following example parses a hash table to `[PSNode]` instance: @@ -36,10 +32,10 @@ The following example parses a hash table to `[PSNode]` instance: PathName Name Depth Value -------- ---- ----- ----- - 0 {My, Object} + 0 {My, Object} ``` -### Example 2: select a sub node in an object graph +### Example 2: select a sub node in an object graph The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) @@ -53,7 +49,7 @@ PathName Name Depth Value My[1] 1 2 2 ``` -### Example 3: Change the price of the **PowerShell** book: +### Example 3: Change the price of the **PowerShell** book: ```PowerShell @@ -99,20 +95,23 @@ for more details, see: [PowerShell Object Parser][1] and [Extended dot notation] ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> The concerned object graph or node. - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Path `** +### `-Path` <Object> Specifies the path to a specific node in the object graph. The path might be either: @@ -121,43 +120,68 @@ The path might be either: * A array of strings (dictionary keys or Property names) and/or integers (list indices) * A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Path +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: Path +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Literal`** +### `-Literal` If Literal switch is set, all (map) nodes in the given path are considered literal. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Literal +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: Path +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` + +### `-ValueOnly` -### **`-Unique`** +returns the value of the node instead of the node itself. + +```powershell +Name: -ValueOnly +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` + +### `-Unique` Specifies that if a subset of the nodes has identical properties and values, only a single node of the subset should be selected. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Unique +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. The failsafe will prevent infinitive loops for circular references as e.g. in: @@ -173,21 +197,26 @@ The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: # None +Type: [Int32] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [PowerShell Object Parser][1] -* 2: [Extended dot notation][2] +* [PowerShell Object Parser](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md) +* [Extended dot notation](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" -[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/XdnPath.md "Extended dot notation" +[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) diff --git a/Docs/Import-ObjectGraph.md b/Docs/Import-ObjectGraph.md index 12adf38..d3eea5f 100644 --- a/Docs/Import-ObjectGraph.md +++ b/Docs/Import-ObjectGraph.md @@ -8,17 +8,16 @@ Deserializes a PowerShell File or any object-graphs from PowerShell file to an o ```PowerShell Import-ObjectGraph -Path + [-ListAs ] + [-MapAs ] + [-LanguageMode ] + [-Encoding ] [] ``` ```PowerShell Import-ObjectGraph -LiteralPath - [] -``` - -```PowerShell -Import-ObjectGraph [-ListAs ] [-MapAs ] [-LanguageMode ] @@ -34,74 +33,90 @@ of strings and values. ## Parameters -### **`-Path `** +### `-Path` <String[]> Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. Wildcard characters are permitted. - - - - - - - -
Type:String[]
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Path +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: Path +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LiteralPath `** +### `-LiteralPath` <String[]> Specifies a path to one or more locations that contain a PowerShell the object-graph. The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell PowerShell not to interpret any characters as escape sequences. - - - - - - - -
Type:String[]
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LiteralPath +Aliases: -PSPath, -LP +Type: [String[]] +Value (default): # Undefined +Parameter sets: LiteralPath +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ListAs `** +### `-ListAs` <Object> -If supplied, the array sub-expression `@( )` syntaxes without an type initializer or with an unknown or +If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or denied type initializer will be converted to the given list type. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ListAs +Aliases: -ArrayAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MapAs `** +### `-MapAs` <Object> -If supplied, the array sub-expression `@{ }` syntaxes without an type initializer or with an unknown or +If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or denied type initializer will be converted to the given map (dictionary or object) type. The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that support explicit type initiators. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MapAs +Aliases: -DictionaryAs +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-LanguageMode `** +### `-LanguageMode` <PSLanguageMode> Defines which object types are allowed for the deserialization, see: [About language modes][2] * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + +```PowerShell `[String]`, `[Array]` or `[HashTable]`. +``` + * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any @@ -117,32 +132,40 @@ other files, which usually concerns PowerShell (`.ps1`) files. > best to design your configuration expressions with restricted or constrained classes, rather than > allowing full freeform expressions. - - - - - - - -
Type:PSLanguageMode
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -LanguageMode +Aliases: # None +Type: [PSLanguageMode] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Encoding `** +### `-Encoding` <Object> Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. - - - - - - - -
Type:Object
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Encoding +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [PowerShell Object Parser][1] -* 2: [About language modes][2] +* [PowerShell Object Parser](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md) +* [About language modes](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" diff --git a/Docs/Merge-ObjectGraph.md b/Docs/Merge-ObjectGraph.md index f5b2457..beb0250 100644 --- a/Docs/Merge-ObjectGraph.md +++ b/Docs/Merge-ObjectGraph.md @@ -21,9 +21,9 @@ Recursively merges two object graphs into a new object graph. ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> -The input object that will be merged with the template object (see: [-Template](#-template) parameter). +The input object that will be merged with the template object (see: [`-Template`](#-template) parameter). > [!NOTE] > Multiple input object might be provided via the pipeline. @@ -34,70 +34,86 @@ The input object that will be merged with the template object (see: [-Template]( ,$InputObject | Compare-ObjectGraph $Template. ``` - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Template `** +### `-Template` <Object> -The template that is used to merge with the input object (see: [-InputObject](#-inputobject) parameter). +The template that is used to merge with the input object (see: [`-InputObject`](#-inputobject) parameter). - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Template +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-PrimaryKey `** +### `-PrimaryKey` <String[]> In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to -link the items or properties: if the PrimaryKey exists on both the [-Template](#-template) and the -[-InputObject](#-inputobject) and the values are equal, the dictionary or PowerShell object will be merged. +link the items or properties: if the PrimaryKey exists on both the [`-Template`](#-template) and the +[`-InputObject`](#-inputobject) and the values are equal, the dictionary or PowerShell object will be merged. Otherwise (if the key can't be found or the values differ), the complete dictionary or PowerShell object will be added to the list. It is allowed to supply multiple primary keys where each primary key will be used to -check the relation between the [-Template](#-template) and the [-InputObject](#-inputobject). - - - - - - - - -
Type:String[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-MatchCase`** - - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
- -### **`-MaxDepth `** - -The maximal depth to recursively compare each embedded property (default: 10). - - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+check the relation between the [`-Template`](#-template) and the [`-InputObject`](#-inputobject). + +```powershell +Name: -PrimaryKey +Aliases: # None +Type: [String[]] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` + +### `-MatchCase` + +```powershell +Name: -MatchCase +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` + +### `-MaxDepth` <Int32> + +The maximal depth to recursively compare each embedded node. +The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). + +```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) diff --git a/Docs/SchemaObject.md b/Docs/SchemaObject.md index 2abbf49..4996225 100644 --- a/Docs/SchemaObject.md +++ b/Docs/SchemaObject.md @@ -60,7 +60,7 @@ to validate the **value** input node. ## Assert nodes The list of existing assert nodes is limited to: -* `AssertTestPrefix` +* `AssertPrefix` * `@Description` * `@References` * `@Type` @@ -88,7 +88,7 @@ The list of existing assert nodes is limited to: * Each assert node describes or constrains the allowed opposite input object node or value as follows: -### `AssertTestPrefix` +### `AssertPrefix` By default, each assert node is prefixed by a single at-sign (`@`) and defines the constrains of the input node value (see [assert nodes](#Assert-nodes) for more details). Any other node object in the test node collection @@ -97,9 +97,9 @@ further defines any child nodes in the schema object branch (see [child nodes](# > [!NOTE] > This "assert node" directive is only accepted at the top level of the schema object and is used to determine > the test node prefix for all other assert nodes. The name of this "assert node" directive might be overruled -> by the `Test-Object -AssertTestPrefix` cmdlet parameter. +> by the `Test-Object -AssertPrefix` cmdlet parameter. -| Name | AssertTestPrefix | +| Name | AssertPrefix | | ----------- | --------------------------------------------- | | Description | Defines the assert prefix of each assert node | | Type | `String` | diff --git a/Docs/Sort-ObjectGraph.md b/Docs/Sort-ObjectGraph.md index 76729d7..a298235 100644 --- a/Docs/Sort-ObjectGraph.md +++ b/Docs/Sort-ObjectGraph.md @@ -1,12 +1,12 @@ -# ConvertTo-SortedObjectGraph +# Invoke-SortObjectGraph -Sort object graph +Sort an object graph ## Syntax ```PowerShell -ConvertTo-SortedObjectGraph +Invoke-SortObjectGraph -InputObject [-PrimaryKey ] [-MatchCase] @@ -17,15 +17,20 @@ ConvertTo-SortedObjectGraph ## Description -Recursively sorts a object graph. +Recursively sorts an object graph. + +> [!WARNING](#warning) +> `Sort-ObjectGraph` is an alias for `Invoke-SortObjectGraph` but to avoid "unapproved verb" warnings during the +> module import a different cmdlet name used. See: +> [Give the script author the ability to disable the unapproved verbs warning][https://github.com/PowerShell/PowerShell/issues/25642] ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> The input object that will be recursively sorted. -> [!NOTE] +> [!NOTE](#note) > Multiple input object might be provided via the pipeline. > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. > To avoid a list of (root) objects to unroll, use the **comma operator**: @@ -34,71 +39,86 @@ The input object that will be recursively sorted. ,$InputObject | Sort-Object. ``` - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: # All +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-PrimaryKey `** +### `-PrimaryKey` <String[]> -Any primary key defined by the [-PrimaryKey](#-primarykey) parameter will be put on top of [-InputObject](#-inputobject) +Any primary key defined by the [`-PrimaryKey`](#-primarykey) parameter will be put on top of [`-InputObject`](#-inputobject) independent of the (descending) sort order. It is allowed to supply multiple primary keys. - - - - - - - -
Type:String[]
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -PrimaryKey +Aliases: -By +Type: [String[]] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MatchCase`** +### `-MatchCase` (Alias `-CaseSensitive`) Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MatchCase +Aliases: -CaseSensitive +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Descending`** +### `-Descending` Indicates that Sort-Object sorts the objects in descending order. The default is ascending order. -> [!NOTE] -> Primary keys (see: [-PrimaryKey](#-primarykey)) will always put on top. - - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+> [!NOTE](#note) +> Primary keys (see: [`-PrimaryKey`](#-primarykey)) will always put on top. + +```powershell +Name: -Descending +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> The maximal depth to recursively compare each embedded property (default: 10). - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: # All +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` [comment]: <> (Created with Get-MarkdownHelp: Install-Script -Name Get-MarkdownHelp) diff --git a/Docs/Test-ObjectGraph.md b/Docs/Test-ObjectGraph.md index f93d094..303b921 100644 --- a/Docs/Test-ObjectGraph.md +++ b/Docs/Test-ObjectGraph.md @@ -38,7 +38,7 @@ The schema object has the following major features: ## Examples -### Example 1: Test whether a `$Person` object meats the schema requirements. +### Example 1: Test whether a `$Person` object meats the schema requirements. ```PowerShell @@ -95,23 +95,26 @@ $Person | Test-Object $Schema | Should -BeNullOrEmpty ## Parameters -### **`-InputObject `** +### `-InputObject` <Object> Specifies the object to test for validity against the schema object. The object might be any object containing embedded (or even recursive) lists, dictionaries, objects or scalar values received from a application or an object notation as Json or YAML using their related `ConvertFrom-*` cmdlets. - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -InputObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: ValidateOnly, ResultList +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-SchemaObject `** +### `-SchemaObject` <Object> Specifies a schema to validate the JSON input against. By default, if any discrepancies, toy will be reported in a object list containing the path to failed node, the value whether the node is valid or not and the issue. @@ -119,72 +122,89 @@ If no issues are found, the output is empty. For details on the schema object, see the [schema object definitions][1] documentation. - - - - - - - -
Type:Object
Mandatory:True
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -SchemaObject +Aliases: # None +Type: [Object] +Value (default): # Undefined +Parameter sets: ValidateOnly, ResultList +Mandatory: True +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-ValidateOnly`** +### `-ValidateOnly` If set, the cmdlet will stop at the first invalid node and return the test result object. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -ValidateOnly +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: ValidateOnly +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-Elaborate`** +### `-Elaborate` If set, the cmdlet will return the test result object for all tested nodes, even if they are valid or ruled out in a possible list node branch selection. - - - - - - - -
Type:SwitchParameter
Mandatory:False
Position:Named
Default value:
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -Elaborate +Aliases: # None +Type: [SwitchParameter] +Value (default): # Undefined +Parameter sets: ResultList +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-AssertTestPrefix `** +### `-AssertTestPrefix` <String> The prefix used to identify the assert test nodes in the schema object. By default, the prefix is `AssertTestPrefix`. - - - - - - - -
Type:String
Mandatory:False
Position:Named
Default value:'AssertTestPrefix'
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -AssertTestPrefix +Aliases: # None +Type: [String] +Value (default): 'AssertTestPrefix' +Parameter sets: ValidateOnly, ResultList +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` -### **`-MaxDepth `** +### `-MaxDepth` <Int32> The maximal depth to recursively test each embedded node. The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). - - - - - - - -
Type:Int32
Mandatory:False
Position:Named
Default value:[PSNode]::DefaultMaxDepth
Accept pipeline input:False
Accept wildcard characters:False
+```powershell +Name: -MaxDepth +Aliases: -Depth +Type: [Int32] +Value (default): [PSNode]::DefaultMaxDepth +Parameter sets: ValidateOnly, ResultList +Mandatory: False +Position: # Named +Accept pipeline input: False +Accept wildcard characters: False +``` ## Related Links -* 1: [Schema object definitions][1] +* [Schema object definitions](https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/SchemaObject.md) + + [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/SchemaObject.md "Schema object definitions" diff --git a/ObjectGraphTools.psd1 b/ObjectGraphTools.psd1 index 87beb69..c503e42 100644 --- a/ObjectGraphTools.psd1 +++ b/ObjectGraphTools.psd1 @@ -3,7 +3,7 @@ RootModule = 'ObjectGraphTools.psm1' # Version number of this module. - ModuleVersion = '0.3.2' + ModuleVersion = '0.3.3' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/ObjectGraphTools.psm1 b/ObjectGraphTools.psm1 index f8b1562..3e17bea 100644 --- a/ObjectGraphTools.psm1 +++ b/ObjectGraphTools.psm1 @@ -6,6 +6,7 @@ using namespace System.Management.Automation using namespace System.Management.Automation.Language using namespace System.Linq.Expressions using namespace System.Reflection +using namespace System.Collections.Specialized #EndRegion Using @@ -101,6 +102,281 @@ Class Abbreviate { } } class LogicalTerm {} +class LogicalOperator : LogicalTerm { + hidden [LogicalOperatorEnum]$Value + LogicalOperator ([LogicalOperatorEnum]$Operator) { $this.Value = $Operator } + LogicalOperator ([String]$Operator) { $this.Value = [LogicalOperatorEnum]$Operator } + [String]ToString() { return $this.Value } +} +class LogicalVariable : LogicalTerm { + hidden [Object]$Value + LogicalVariable ($Variable) { $this.Value = $Variable } + [String]ToString() { + if ($this.Value -is [String]) { + return "'$($this.Value -Replace "'", "''")'" + } + else { return $this.Value } + } +} +class LogicalFormula : LogicalTerm { + hidden static $OperatorSymbols = @{ + '!' = [LogicalOperatorEnum]'Not' + ',' = [LogicalOperatorEnum]'And' + '*' = [LogicalOperatorEnum]'And' + '|' = [LogicalOperatorEnum]'Or' + '+' = [LogicalOperatorEnum]'Or' + } + hidden static [Int[]]$OperatorNameLengths + + hidden [List[LogicalTerm]]$Terms = [List[LogicalTerm]]::new() + hidden [Int]$Pointer + + GetFormula([String]$Expression, [Int]$Start) { + $SubExpression = $Start -gt 0 + $InString = $null # Quote type (double - or single quoted) + $Escaped = $null + $this.Pointer = $Start + While ($this.Pointer -le $Expression.Length) { + $Char = if ($this.Pointer -lt $Expression.Length) { $Expression[$this.Pointer] } + if ($InString) { + if ($Char -eq $InString) { + if ($this.Pointer + 1 -lt $Expression.Length -and $Expression[$this.Pointer + 1] -eq $InString) { + $Escaped = $true + $this.Pointer++ + } + else { + $Name = $Expression.SubString($Start + 1, ($this.Pointer - $Start - 1)) + if ($Escaped) { $Name = $Name.Replace("$InString$InString", $InString) } + $this.Terms.Add([LogicalVariable]::new($Name)) + $InString = $Null + $Start = $this.Pointer + 1 + } + } + } + elseif ('"', "'" -eq $Char) { + $InString = $Char + $Escaped = $false + $Start = $this.Pointer + } + elseif ($Char -eq '(') { + $Formula = [LogicalFormula]::new($Expression, ($this.Pointer + 1)) + $this.Terms.Add($Formula) + $this.Pointer = $Formula.Pointer + $Start = $this.Pointer + 1 + } + elseif ($Char -in $Null, ' ', ')' + [LogicalFormula]::OperatorSymbols.Keys) { + $Length = $this.Pointer - $Start + if ($Length -gt 0) { + $Term = $Expression.SubString($Start, $Length) + if ([LogicalOperatorEnum].GetEnumNames() -eq $Term) { + $this.Terms.Add([LogicalOperator]::new($Term)) + } + else { + $Double = 0 + if ([double]::TryParse($Term, [Ref]$Double)) { + $this.Terms.Add([LogicalVariable]::new($Double)) + } + else { + $this.Terms.Add([LogicalVariable]::new($Term)) + } + } + } + if ($Char -eq ')') { return } + if ($Char -gt ' ') { + $this.Terms.Add([LogicalOperator]::new([LogicalFormula]::OperatorSymbols($Char))) + } + $Start = $this.Pointer + 1 + } + $this.Pointer++ + } + if ($InString) { Throw "Missing the terminator: $InString in logical expression: $Expression" } + if ($SubExpression) { Throw "Missing closing ')' in logical expression: $Expression" } + } + + LogicalFormula() {} + LogicalFormula ([String]$Expression) { + $this.GetFormula($Expression, 0) + if ($this.Pointer -lt $Expression.Length) { + Throw "Unexpected token ')' at position $($this.Pointer) in logical expression: $Expression" + } + } + LogicalFormula ([String]$Expression, $Start) { + $this.GetFormula($Expression, $Start) + } + + Append ([LogicalOperator]$Operator, $Formula) { + if ($Formula -is [LogicalFormula]) { + if ($Formula.Terms.Count -eq 0) { return } + if($this.Terms.Count -eq 0) { + $This.Terms = $Formula.Terms + return + } + if ($Operator.Value -eq 'Not') { $this.Terms.Add([LogicalOperator]'And') } + $this.Terms.Add($Operator) + if ($Formula.Terms.Count -gt 1) { $this.Terms.Add($Formula) } + else { $this.Terms.Add($Formula.Terms[0]) } + } + elseif ($null -ne $Formula) { + if ($this.Terms.Count -gt 0) { $this.Terms.Add([LogicalOperator]'And') } + $this.Terms.Add([LogicalVariable]::new($Formula)) + } + } + And($Formula) { $this.Append('And', $Formula) } + Or($Formula) { $this.Append('Or', $Formula) } + Xor($Formula) { $this.Append('Xor', $Formula) } + + [Object]Find([ScriptBlock]$Predicate, [Bool]$All) { + $Stack = [Stack]::new() + $Enumerator = $this.Terms.GetEnumerator() + $Term = $null + return $( + while ($true) { + while ($Enumerator.MoveNext()) { + $Term = $Enumerator.Current + if ($Term -is [LogicalFormula]) { + $Stack.Push($Enumerator) + $Enumerator = $Term.Terms.GetEnumerator() + $Term = $null + continue + } + else { + if (& $Predicate $Term) { if ($All) { $Term } else { return $Term } } + } + } + if (-not $Stack.Count) { break } + $Enumerator = $Stack.Pop() + } + ) + } + + [bool]Evaluate($Variables) { + if (-not $this.Terms) { return $null -eq $Variables -or -not $Variables.Count } + $Enumerator = $this.Terms.GetEnumerator() + $Stack = [Stack]::new() + $Stack.Push(@{ + Enumerator = $Enumerator + Accumulator = $null + Operator = $null + Negate = $null + }) + $Term, $Negate, $Operand, $Operator, $Accumulator = $null + $Score = 0 + while ($Stack.Count -gt 0) { + # Accumulator = Accumulator Operand + # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} + $Pop = $Stack.Pop() + $Enumerator = $Pop.Enumerator + $Operator = $Pop.Operator + if ($null -eq $Operator) { $Operand = $Pop.Accumulator } + else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } + $Negate = $Pop.Negate + $Compute = $null -notin $Operand, $Operator, $Accumulator + while ($Compute -or $Enumerator.MoveNext()) { + if ($Compute) { $Compute = $false } + else { + $Term = $Enumerator.Current + if ($Term -is [LogicalVariable]) { + if ($Variables -is [ScriptBlock]) { $Operand = [Bool]$Term.Value.foreach($Variables) } + elseif ($Variables -is [IDictionary]) { $Operand = [Bool]$Variables[$Term.Value] } + elseif ($Variables -is [IEnumerable]) { $Operand = $Variables.Contains($Term.Value) } + else { $Operand = $Variables -eq $Term.Value } + } + elseif ($Term -is [LogicalOperator]) { + if ($Term.Value -eq 'Not') { $Negate = -not $Negate } + elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } + else { throw [InvalidOperationException]"Unknown logical operator: $Term" } + } + elseif ($Term -is [LogicalFormula]) { + $Stack.Push(@{ + Enumerator = $Enumerator + Accumulator = $Accumulator + Operator = $Operator + Negate = $Negate + }) + $Accumulator, $Operator, $Negate = $null + $Enumerator = $Term.Terms.GetEnumerator() + continue + } + else { throw [InvalidOperationException]"Unknown logical term: $Term" } + } + if ($null -ne $Operand) { + $Score++ + if ($null -eq $Accumulator -xor $null -eq $Operator) { + if ($Accumulator) { throw [InvalidOperationException]"Missing operator before: $Term" } + else { throw [InvalidOperationException]"Missing variable before: $Operator $Term" } + } + $Operand = $Operand -xor $Negate + $Negate = $null + if ($Operator -eq 'And') { + $Operator = $null + if ($Accumulator -eq $false) { break } + $Accumulator = $Accumulator -and $Operand + } + elseif ($Operator -eq 'Or') { + $Operator = $null + if ($Accumulator -eq $true) { break } + $Accumulator = $Accumulator -or $Operand + } + elseif ($Operator -eq 'Xor') { + $Operator = $null + $Accumulator = $Accumulator -xor $Operand + } + else { $Accumulator = $Operand } + $Operand = $Null + } + } + if ($null -ne $Operator -or $null -ne $Negate) { throw [InvalidOperationException]"Missing variable after $Operator" } + } + if ($null -eq $Accumulator) { throw "The accumulator isn't defined" } + return $Accumulator + } + + [String] ToString() { return $this.ToString($null) } + [String] ToString([IDictionary]$Extents) { + $StringBuilder = [System.Text.StringBuilder]::new() + $Stack = [Stack]::new() + $Enumerator = $this.Terms.GetEnumerator() + $Term = $null + while ($true) { + while ($Enumerator.MoveNext()) { + if ($Null -ne $Term) { + $null = $StringBuilder.Append([ANSI]::ResetColor) # Not really necessarily + $null = $StringBuilder.Append(' ') + } + $Term = $Enumerator.Current + if ($Term -is [LogicalVariable]) { + if ($Term.Value -is [String]) { $null = $StringBuilder.Append([ANSI]::VariableColor) } + else { $null = $StringBuilder.Append([ANSI]::NumberColor) } + $null = $StringBuilder.Append($Term) + if ($Extents) { + $null = $StringBuilder.Append([ANSI]::EmphasisColor) + $null = $StringBuilder.Append($Extents[$Term.Value]) + } + } + elseif ($Term -is [LogicalOperator]) { + $null = $StringBuilder.Append([ANSI]::OperatorColor) + $null = $StringBuilder.Append($Term) + } + else { # if ($Term -is [LogicalFormula]) + $null = $StringBuilder.Append([ANSI]::StringColor) + $null = $StringBuilder.Append('(') + $Stack.Push($Enumerator) + $Enumerator = $Term.Terms.GetEnumerator() + $Term = $null + continue + } + } + if (-not $Stack.Count) { + $null = $StringBuilder.Append([ANSI]::ResetColor) + break + } + $null = $StringBuilder.Append([ANSI]::StringColor) + $null = $StringBuilder.Append(')') + $Enumerator = $Stack.Pop() + } + return $StringBuilder.ToString() + } +} Class PSNodePath { hidden [PSNode[]]$Nodes hidden [String]$_String @@ -226,7 +502,7 @@ Class PSNode : IComparable { return $Node } - static [PSNode] ParseInput($Object) { return [PSNode]::parseInput($Object, 0) } + static [PSNode] ParseInput($Object) { return [PSNode]::ParseInput($Object, 0) } static [int] Compare($Left, $Right) { return [ObjectComparer]::new().Compare($Left, $Right) @@ -324,1874 +600,1737 @@ Class PSNode : IComparable { } hidden CollectNodes($NodeTable, [XdnPath]$Path, [Int]$PathIndex) { + if ($PathIndex -ge $Path.Entries.Count) { + $NodeTable[$this.getPathName()] = $this + return + } $Entry = $Path.Entries[$PathIndex] - $NextIndex = if ($PathIndex -lt $Path.Entries.Count -1) { $PathIndex + 1 } - $NextEntry = if ($NextIndex) { $Path.Entries[$NextIndex] } - $Equals = if ($NextEntry -and $NextEntry.Key -eq 'Equals') { - $NextEntry.Value - $NextIndex = if ($NextIndex -lt $Path.Entries.Count -1) { $NextIndex + 1 } - } - switch ($Entry.Key) { - Root { - $Node = $this.RootNode - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } + if ($Entry.Key -eq 'Root') { + $this.RootNode.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + } + elseif ($Entry.Key -eq 'Ancestor') { + $Node = $this + for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode } + if ($i -eq 0) { $Node.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } + } + elseif ($Entry.Key -eq 'Index') { + if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) { + $this.GetChildNode([Int]$Entry.Value).CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } - Ancestor { - $Node = $this - for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode } - if ($i -eq 0) { # else: reached root boundary - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } + } + elseif ($Entry.Key -eq 'Equals') { + if ($this -is [PSLeafNode]) { + foreach ($Value in $Entry.Value) { + if ($this._Value -like $Value) { + $this.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + break + } } } - Index { - if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) { - $Node = $this.GetChildNode([Int]$Entry.Value) - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } + } + elseif ($this -is [PSListNode]) { # Member access enumeration + foreach ($Node in $this.get_ChildNodes()) { + $Node.CollectNodes($NodeTable, $Path, $PathIndex) } - Default { # Child, Descendant - if ($this -is [PSListNode]) { # Member access enumeration - foreach ($Node in $this.get_ChildNodes()) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) + } + elseif ($this -is [PSMapNode]) { + $Count0 = $NodeTable.get_Count() + foreach ($Value in $Entry.Value) { + $Name = $Value._Value + if ($Value.ContainsWildcard()) { + $CaseMatters = $this.CaseMatters + foreach ($Node in $this.ChildNodes) { + if ($CaseMatters) { if ($Node.Name -cnotlike $Name) { continue } } + else { if ($Node.Name -notlike $Name) { continue } } + $Node.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } } - elseif ($this -is [PSMapNode]) { - $Found = $False - $ChildNodes = $this.get_ChildNodes() - foreach ($Node in $ChildNodes) { - if ($Entry.Value -eq $Node.Name -and (-not $Equals -or ($Node -is [PSLeafNode] -and $Equals -eq $Node._Value))) { - $Found = $True - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - } - if (-not $Found -and $Entry.Key -eq 'Descendant') { - foreach ($Node in $ChildNodes) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) - } - } + elseif ($this.Contains($Name)) { + $this.GetChildNode($Name).CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + } + } + if ( + ($Entry.Key -eq 'Offspring') -or + ($Entry.Key -eq 'Descendant' -and $NodeTable.get_Count() -eq $Count0) + ) { + foreach ($Node in $this.get_ChildNodes()) { + $Node.CollectNodes($NodeTable, $Path, $PathIndex) } } } } - [Object] GetNode([XdnPath]$Path) { + + [Object]GetNode([XdnPath]$Path) { $NodeTable = [system.collections.generic.dictionary[String, PSNode]]::new() # Case sensitive (case insensitive map nodes use the same name) $this.CollectNodes($NodeTable, $Path, 0) if ($NodeTable.Count -eq 0) { return @() } if ($NodeTable.Count -eq 1) { return $NodeTable[$NodeTable.Keys] } else { return [PSNode[]]$NodeTable.Values } } -} -class ObjectComparer { - # Report properties (column names) - [String]$Name1 = 'Reference' - [String]$Name2 = 'InputObject' - [String]$Issue = 'Discrepancy' - - [String[]]$PrimaryKey - [ObjectComparison]$ObjectComparison + [string]ToString() { + return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" + } - [Collections.Generic.List[Object]]$Differences + hidden [string]get_DisplayValue() { + return "$([TypeColor][PSSerialize]::new($this._Value, [PSLanguageMode]'NoLanguage'))" + } +} +Class PSLeafNode : PSNode { - ObjectComparer () {} - ObjectComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - ObjectComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - ObjectComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - ObjectComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } + hidden MaxDepthReached() { } # return nothing knowing that leaf nodes terminate the path anyways - [bool] IsEqual ($Object1, $Object2) { return $this.Compare($Object1, $Object2, 'Equals') } - [int] Compare ($Object1, $Object2) { return $this.Compare($Object1, $Object2, 'Compare') } - [Object] Report ($Object1, $Object2) { - $this.Differences = [Collections.Generic.List[Object]]::new() - $null = $this.Compare($Object1, $Object2, 'Report') - return $this.Differences + hidden PSLeafNode($Object) { + if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } } - [Object] Compare($Object1, $Object2, [ObjectCompareMode]$Mode) { - if ($Object1 -is [PSNode]) { $Node1 = $Object1 } else { $Node1 = [PSNode]::ParseInput($Object1) } - if ($Object2 -is [PSNode]) { $Node2 = $Object2 } else { $Node2 = [PSNode]::ParseInput($Object2) } - return $this.CompareRecurse($Node1, $Node2, $Mode) + [Int]GetHashCode($CaseSensitive) { + if ($Null -eq $this._Value) { return '$Null'.GetHashCode() } + if ($CaseSensitive) { return $this._Value.GetHashCode() } + else { + if ($this._Value -is [String]) { return $this._Value.ToUpper().GetHashCode() } # Windows PowerShell doesn't have a System.HashCode structure + else { return $this._Value.GetHashCode() } + } } +} +Class PSCollectionNode : PSNode { + hidden static PSCollectionNode() { Use-ClassAccessors } + hidden [Dictionary[bool,int]]$_HashCode # Unlike the value HashCode, the default (bool = $false) node HashCode is case insensitive + hidden [Dictionary[bool,int]]$_ReferenceHashCode # if changed, recalculate the (bool = case sensitive) node's HashCode - hidden [Object] CompareRecurse([PSNode]$Node1, [PSNode]$Node2, [ObjectCompareMode]$Mode) { - $Comparison = $this.ObjectComparison - $MatchCase = $Comparison -band 'MatchCase' - $EqualType = $true + hidden [bool]MaxDepthReached() { + # Check whether the max depth has been reached. + # Warn if it has, but suppress the warning if + # it took less then 5 seconds since the last + # time it reached the max depth. + $MaxDepthReached = $this.Depth -ge $this.RootNode._MaxDepth + if ($MaxDepthReached) { + if (([DateTime]::Now - $this.RootNode.MaxDepthWarningTime).TotalSeconds -gt 5) { + Write-Warning "$($this.Path) reached the maximum depth of $($this.RootNode._MaxDepth)." + } + $this.RootNode.MaxDepthWarningTime = [DateTime]::Now + } + return $MaxDepthReached + } - if ($Mode -ne 'Compare') { - if ($MatchCase -and $Node1.ValueType -ne $Node2.ValueType) { - if ($Mode -eq 'Equals') { return $false } else { # if ($Mode -eq 'Report') - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Type' - $this.Name1 = $Node1.ValueType - $this.Name2 = $Node2.ValueType - }) - } - } - if ($Node1 -is [PSCollectionNode] -and $Node2 -is [PSCollectionNode] -and $Node1.Count -ne $Node2.Count) { - if ($Mode -eq 'Equals') { return $false } else { # if ($Mode -eq 'Report') - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Size' - $this.Name1 = $Node1.Count - $this.Name2 = $Node2.Count - }) - } - } + hidden WarnSelector ([PSCollectionNode]$Node, [String]$Name) { + if ($Node -is [PSListNode]) { + $SelectionName = "'$Name'" + $CollectionType = 'list' } + else { + $SelectionName = "[$Name]" + $CollectionType = 'list' + } + Write-Warning "Expected $SelectionName to be a $CollectionType selector for: $($Node.Path)" + } - if ($Node1 -is [PSLeafNode] -and $Node2 -is [PSLeafNode]) { - $Eq = if ($MatchCase) { $Node1.Value -ceq $Node2.Value } else { $Node1.Value -eq $Node2.Value } - Switch ($Mode) { - Equals { return $Eq } - Compare { - if ($Eq) { return 1 - $EqualType } # different types results in 1 (-gt) - else { - $Greater = if ($MatchCase) { $Node1.Value -cgt $Node2.Value } else { $Node1.Value -gt $Node2.Value } - if ($Greater -xor $Comparison -band 'Descending') { return 1 } else { return -1 } - } - } - default { - if (-not $Eq) { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Value' - $this.Name1 = $Node1.Value - $this.Name2 = $Node2.Value - }) - } - } - } + hidden [List[Ast]] GetAstSelectors ($Ast) { + $List = [List[Ast]]::new() + if ($Ast -isnot [Ast]) { + $Ast = [Parser]::ParseInput("`$_$Ast", [ref]$Null, [ref]$Null) + $Ast = $Ast.EndBlock.Statements.PipeLineElements.Expression } - elseif ($Node1 -is [PSListNode] -and $Node2 -is [PSListNode]) { - $MatchOrder = -not ($Comparison -band 'IgnoreListOrder') - # if ($Node1.GetHashCode($MatchCase) -eq $Node2.GetHashCode($MatchCase)) { - # if ($Mode -eq 'Equals') { return $true } else { return 0 } # Report mode doesn't care about the output - # } - $Items1 = $Node1.ChildNodes - $Items2 = $Node2.ChildNodes - if (-not $Items1 -and -not $Items2) { - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - if ($Items1.Count) { $Indices1 = [Collections.Generic.List[Int]]$Items1.Name } else { $Indices1 = @() } - if ($Items2.Count) { $Indices2 = [Collections.Generic.List[Int]]$Items2.Name } else { $Indices2 = @() } - if ($this.PrimaryKey) { - $Maps2 = [Collections.Generic.List[Int]]$Items2.where{ $_ -is [PSMapNode] }.Name - if ($Maps2.Count) { - $Maps1 = [Collections.Generic.List[Int]]$Items1.where{ $_ -is [PSMapNode] }.Name - if ($Maps1.Count) { - foreach ($Key in $this.PrimaryKey) { - foreach($Index2 in @($Maps2)) { - $Item2 = $Items2[$Index2] - foreach ($Index1 in @($Maps1)) { - $Item1 = $Items1[$Index1] - if ($Item1.GetValue($Key) -eq $Item2.GetValue($Key)) { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - Switch ($Mode) { - Equals { if (-not $Compare) { return $Compare } } - Compare { if ($Compare) { return $Compare } } - } - $Null = $Indices1.Remove($Index1) - $null = $Indices2.Remove($Index2) - $Null = $Maps1.Remove($Index1) - $null = $Maps2.Remove($Index2) - break # Only match the first primary key - } - } - } - } - # in case of any single maps leftover without primary keys - if($Maps2.Count -eq 1 -and $Maps1.Count -eq 1) { Write-Host - $Item1 = $Items1[$Maps1[0]] - $Item2 = $Items2[$Maps2[0]] - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - Switch ($Mode) { - Equals { if (-not $Compare) { return $Compare } } - Compare { if ($Compare) { return $Compare } } - } - $null = $Indices2.Remove($Maps2[0]) - $Null = $Indices1.Remove($Maps1[0]) - $Maps2.Clear() - $Maps1.Clear() - } - } - } - } - if (-not $MatchOrder) { # remove the equal nodes from the lists - foreach($Index2 in @($Indices2)) { - $Item2 = $Items2[$Index2] - foreach ($Index1 in $Indices1) { - $Item1 = $Items1[$Index1] - if ($this.CompareRecurse($Item1, $Item2, 'Equals')) { - $null = $Indices2.Remove($Index2) - $Null = $Indices1.Remove($Index1) - break # Only match a single node - } - } - } - } - for ($i = 0; $i -lt [math]::max($Indices2.Count, $Indices1.Count); $i++) { - $Index1 = if ($i -lt $Indices1.Count) { $Indices1[$i] } - $Index2 = if ($i -lt $Indices2.Count) { $Indices2[$i] } - $Item1 = if ($Null -ne $Index1) { $Items1[$Index1] } - $Item2 = if ($Null -ne $Index2) { $Items2[$Index2] } - if ($Null -eq $Item1) { - Switch ($Mode) { - Equals { return $false } - Compare { return -1 } # None existing items can't be ordered - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path + "[$Index2]" - $this.Issue = 'Exists' - $this.Name1 = $Null - $this.Name2 = if ($Item2 -is [PSLeafNode]) { "$($Item2.Value)" } else { "[$($Item2.ValueType)]" } - }) - } - } - } - elseif ($Null -eq $Item2) { - Switch ($Mode) { - Equals { return $false } - Compare { return 1 } # None existing items can't be ordered - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.Path + "[$Index1]" - $this.Issue = 'Exists' - $this.Name1 = if ($Item1 -is [PSLeafNode]) { "$($Item1.Value)" } else { "[$($Item1.ValueType)]" } - $this.Name2 = $Null - }) - } - } - } - else { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - } - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } + if ($Ast -is [IndexExpressionAst]) { + $List.AddRange($this.GetAstSelectors($Ast.Target)) + $List.Add($Ast) } - elseif ($Node1 -is [PSMapNode] -and $Node2 -is [PSMapNode]) { - $Items1 = $Node1.ChildNodes - $Items2 = $Node2.ChildNodes - if (-not $Items1 -and -not $Items2) { - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - $MatchOrder = [Bool]($Comparison -band 'MatchMapOrder') - if ($MatchOrder -and $Node1._Value -isnot [HashTable] -and $Node2._Value -isnot [HashTable]) { - $Index = 0 - foreach ($Item1 in $Items1) { - if ($Index -lt $Items2.Count) { $Item2 = $Items2[$Index++] } else { break } - $EqualName = if ($MatchCase) { $Item1.Name -ceq $Item2.Name } else { $Item1.Name -eq $Item2.Name } - if ($EqualName) { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - else { - Switch ($Mode) { - Equals { return $false } - Compare {} # The order depends on the child name and value - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Item1.Path - $this.Issue = 'Name' - $this.Name1 = $Item1.Name - $this.Name2 = $Item2.Name - }) - } - } - } - } - } - else { - $Found = [HashTable]::new() # (Case sensitive) - foreach ($Item2 in $Items2) { - if ($Node1.Contains($Item2.Name)) { - $Item1 = $Node1.GetChildNode($Item2.Name) # Left defines the comparer - $Found[$Item1.Name] = $true - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - else { - Switch ($Mode) { - Equals { return $false } - Compare { return -1 } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Item2.Path - $this.Issue = 'Exists' - $this.Name1 = $false - $this.Name2 = $true - }) - } - } - } - } - foreach ($Name in $Node1.Names) { - if (-not $Found.Contains($Name)) { - Switch ($Mode) { - Equals { return $false } - Compare { return 1 } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.GetChildNode($Name).Path - $this.Issue = 'Exists' - $this.Name1 = $true - $this.Name2 = $false - }) - } - } - } - } - } - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } + elseif ($Ast -is [MemberExpressionAst]) { + $List.AddRange($this.GetAstSelectors($Ast.Expression)) + $List.Add($Ast) } - else { # Different structure - Switch ($Mode) { - Equals { return $false } - Compare { # Structure order: PSLeafNode - PSListNode - PSMapNode (can't be reversed) - if ($Node1 -is [PSLeafNode] -or $Node2 -isnot [PSMapNode] ) { return -1 } else { return 1 } - } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.Path - $this.Issue = 'Structure' - $this.Name1 = $Node1.ValueType.Name - $this.Name2 = $Node2.ValueType.Name - }) + elseif ($Ast.Extent.Text -ne '$_') { + Throw "Parse error: $($Ast.Extent.Text)" + } + return $List + } + + [List[PSNode]]GetNodeList($Levels, [Bool]$LeafNodesOnly) { + $NodeList = [List[PSNode]]::new() + $Stack = [Stack]::new() + $Stack.Push($this.get_ChildNodes().GetEnumerator()) + $Level = 1 + While ($Stack.Count -gt 0) { + $Enumerator = $Stack.Pop() + $Level-- + while ($Enumerator.MoveNext()) { + $Node = $Enumerator.Current + if ($Node.MaxDepthReached() -or ($Levels -ge 0 -and $Level -ge $Levels)) { break } + if (-not $LeafNodesOnly -or $Node -is [PSLeafNode]) { $NodeList.Add($Node) } + if ($Node -is [PSCollectionNode]) { + $Stack.Push($Enumerator) + $Level++ + $Enumerator = $Node.get_ChildNodes().GetEnumerator() } } } - if ($Mode -eq 'Equals') { throw 'Equals comparison should have returned boolean.' } - if ($Mode -eq 'Compare') { throw 'Compare comparison should have returned integer.' } - return @() + return $NodeList } -} -class PSMapNodeComparer : IComparer[Object] { - [String[]]$PrimaryKey - [ObjectComparison]$ObjectComparison + [List[PSNode]]GetNodeList() { return $this.GetNodeList(1, $False) } + [List[PSNode]]GetNodeList([Int]$Levels) { return $this.GetNodeList($Levels, $False) } + hidden [PSNode[]]get_DescendantNodes() { return $this.GetNodeList(-1, $False) } + hidden [PSNode[]]get_LeafNodes() { return $this.GetNodeList(-1, $True) } - PSMapNodeComparer () {} - PSMapNodeComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - PSMapNodeComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - PSMapNodeComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - PSMapNodeComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - [int] Compare ([Object]$Node1, [Object]$Node2) { - $Comparison = $this.ObjectComparison - $MatchCase = $Comparison -band 'MatchCase' - $Equal = if ($MatchCase) { $Node1.Name -ceq $Node2.Name } else { $Node1.Name -eq $Node2.Name } - if ($Equal) { return 0 } - else { - if ($this.PrimaryKey) { # Primary keys take always priority - if ($this.PrimaryKey -eq $Node1.Name) { return -1 } - if ($this.PrimaryKey -eq $Node2.Name) { return 1 } - } - $Greater = if ($MatchCase) { $Node1.Name -cgt $Node2.Name } else { $Node1.Name -gt $Node2.Name } - if ($Greater -xor $Comparison -band 'Descending') { return 1 } else { return -1 } + Sort() { $this.Sort($Null, 0) } + Sort([ObjectComparison]$ObjectComparison) { $this.Sort($Null, $ObjectComparison) } + Sort([String[]]$PrimaryKey) { $this.Sort($PrimaryKey, 0) } + Sort([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.Sort($PrimaryKey, $ObjectComparison) } + Sort([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { + # As the child nodes are sorted first, we just do a side-by-side node compare: + $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' + $ObjectComparison = $ObjectComparison -band (-1 - [ObjectComparison]'IgnoreListOrder') + $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } + $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } + $this.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) + } + + hidden SortRecurse([PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { + $NodeList = $this.GetNodeList() + foreach ($Node in $NodeList) { + if ($Node -is [PSCollectionNode]) { $Node.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) } + } + if ($this -is [PSListNode]) { + $NodeList.Sort($PSListNodeComparer) + if ($NodeList.Count) { $this._Value = @($NodeList.Value) } else { $this._Value = @() } + } + else { # if ($Node -is [PSMapNode]) + $NodeList.Sort($PSMapNodeComparer) + $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) + foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) + if ($this -is [PSObjectNode]) { $this._Value = [PSCustomObject]$Properties } else { $this._Value = $Properties } } } } -Class PSDeserialize { - hidden static [String[]]$Parameters = 'LanguageMode', 'ArrayType', 'HashTableType' - hidden static PSDeserialize() { Use-ClassAccessors } +Class PSListNode : PSCollectionNode { + hidden static PSListNode() { Use-ClassAccessors } - hidden $_Object - [PSLanguageMode]$LanguageMode = 'Restricted' - [Type]$ArrayType = 'Array' -as [Type] - [Type]$HashTableType = 'HashTable' -as [Type] - [String] $Expression + hidden PSListNode($Object) { + if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } + } - PSDeserialize([String]$Expression) { $this.Expression = $Expression } - PSDeserialize( - $Expression, - $LanguageMode = 'Restricted', - $ArrayType = $Null, - $HashTableType = $Null - ) { - if ($this.LanguageMode -eq 'NoLanguage') { # No language mode is internally used for displaying - Throw 'The language mode "NoLanguage" is not supported.' - } - $this.Expression = $Expression - $this.LanguageMode = $LanguageMode - if ($Null -ne $ArrayType) { $this.ArrayType = $ArrayType } - if ($Null -ne $HashTableType) { $this.HashTableType = $HashTableType } + hidden [Object]get_Count() { + return $this._Value.get_Count() } - hidden [Object] get_Object() { - if ($Null -eq $this._Object) { - $Ast = [System.Management.Automation.Language.Parser]::ParseInput($this.Expression, [ref]$null, [ref]$Null) - $this._Object = $this.ParseAst([Ast]$Ast) - } - return $this._Object + hidden [Object]get_Names() { + if ($this._Value.Length) { return ,@(0..($this._Value.Length - 1)) } + return ,@() } - hidden [Object] ParseAst([Ast]$Ast) { - # Write-Host 'Ast type:' "$($Ast.getType())" - $Type = $Null - if ($Ast -is [ConvertExpressionAst]) { - $FullTypeName = $Ast.Type.TypeName.FullName - if ( - $this.LanguageMode -eq 'Full' -or ( - $this.LanguageMode -eq 'Constrained' -and - [PSLanguageType]::IsConstrained($FullTypeName) - ) - ) { - try { $Type = $FullTypeName -as [Type] } catch { write-error $_ } - } - $Ast = $Ast.Child - } - if ($Ast -is [ScriptBlockAst]) { - $List = [List[Object]]::new() - if ($Null -ne $Ast.BeginBlock) { $Ast.BeginBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($Null -ne $Ast.ProcessBlock) { $Ast.ProcessBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($Null -ne $Ast.EndBlock) { $Ast.EndBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($List.Count -eq 1) { return $List[0] } else { return @($List) } - } - elseif ($Ast -is [PipelineAst]) { - $Elements = $Ast.PipelineElements - if (-not $Elements.Count) { return @() } - elseif ($Elements -is [CommandAst]) { - return $Null #85 ConvertFrom-Expression: convert function/cmdlet calls to Objects - } - elseif ($Elements.Expression.Count -eq 1) { return $this.ParseAst($Elements.Expression[0]) } - else { return $Elements.Expression.Foreach{ $this.ParseAst($_) } } - } - elseif ($Ast -is [ArrayLiteralAst] -or $Ast -is [ArrayExpressionAst]) { - if (-not $Type -or 'System.Object[]', 'System.Array' -eq $Type.FullName) { $Type = $this.ArrayType } - if ($Ast -is [ArrayLiteralAst]) { $Value = $Ast.Elements.foreach{ $this.ParseAst($_) } } - else { $Value = $Ast.SubExpression.Statements.foreach{ $this.ParseAst($_) } } - if ('System.Object[]', 'System.Array' -eq $Type.FullName) { - if ($Value -isnot [Array]) { $Value = @($Value) } # Prevent single item array unrolls - } - else { $Value = $Value -as $Type } - return $Value - } - elseif ($Ast -is [HashtableAst]) { - if (-not $Type -or $Type.FullName -eq 'System.Collections.Hashtable') { $Type = $this.HashTableType } - $IsPSCustomObject = "$Type" -in - 'PSCustomObject', - 'System.Management.Automation.PSCustomObject', - 'PSObject', - 'System.Management.Automation.PSObject' - if ($Type.FullName -eq 'System.Collections.Hashtable') { $Map = @{} } # Case insensitive - elseif ($IsPSCustomObject) { $Map = [Ordered]@{} } - else { $Map = New-Object -Type $Type } - $Ast.KeyValuePairs.foreach{ - if ( $Map -is [Collections.IDictionary]) { $Map.Add($_.Item1.Value, $this.ParseAst($_.Item2)) } - else { $Map."$($_.Item1.Value)" = $this.ParseAst($_.Item2) } - } - if ($IsPSCustomObject) { return [PSCustomObject]$Map } else { return $Map } - } - elseif ($Ast -is [ConstantExpressionAst]) { - if ($Type) { $Value = $Ast.Value -as $Type } else { $Value = $Ast.Value } - return $Value - } - elseif ($Ast -is [VariableExpressionAst]) { - $Value = switch ($Ast.VariablePath.UserPath) { - Null { $Null } - True { $True } - False { $False } - PSCulture { (Get-Culture).ToString() } - PSUICulture { (Get-UICulture).ToString() } - Default { $Ast.Extent.Text } - } - return $Value - } - else { return $Null } + hidden [Object]get_Values() { + return ,@($this._Value) + } + + hidden [Object]get_CaseMatters() { return $false } + + [Bool]Contains($Index) { + return $Index -ge 0 -and $Index -lt $this.get_Count() + } + + [Bool]Exists($Index) { + return $Index -ge 0 -and $Index -lt $this.get_Count() + } + + [Object]GetValue($Index) { return $this._Value[$Index] } + [Object]GetValue($Index, $Default) { + if (-not $This.Contains($Index)) { return $Default } + return $this._Value[$Index] + } + + SetValue($Index, $Value) { + if ($Value -is [PSNode]) { $Value = $Value.Value } + $this._Value[$Index] = $Value } -} -Class PSInstance { - static [Object]Create($Object) { - if ($Null -eq $Object) { return $Null } - elseif ($Object -is [String]) { - $String = if ($Object.StartsWith('[') -and $Object.EndsWith(']')) { $Object.SubString(1, ($Object.Length - 2)) } else { $Object } - Switch -Regex ($String) { - '^((System\.)?String)?$' { return '' } - '^(System\.)?Array$' { return ,@() } - '^(System\.)?Object\[\]$' { return ,@() } - '^((System\.)?Collections\.Hashtable\.)?hashtable$' { return @{} } - '^((System\.)?Management\.Automation\.)?ScriptBlock$' { return {} } - '^((System\.)?Collections\.Specialized\.)?Ordered(Dictionary)?$' { return [Ordered]@{} } - '^((System\.)?Management\.Automation\.)?PS(Custom)?Object$' { return [PSCustomObject]@{} } + + Add($Value) { + if ($Value -is [PSNode]) { $Value = $Value._Value } + if ($this._Value.GetType().GetMethod('Add')) { $null = $This._Value.Add($Value) } + else { $this._Value = ($this._Value + $Value) -as $this._Value.GetType() } + $this.Cache.Remove('ChildNodes') + } + + Remove($Value) { + if ($Value -is [PSNode]) { $Value = $Value.Value } + if (-not $this.Value.Contains($Value)) { return } + if ($this.Value.GetType().GetMethod('Remove')) { $null = $this._value.remove($Value) } + else { + $cList = [List[Object]]::new() + $iList = [List[Object]]::new() + $ceq = $false + foreach ($ChildNode in $this.get_ChildNodes()) { + if (-not $ceq -and $ChildNode.Value -ceq $Value) { $ceq = $true } else { $cList.Add($ChildNode.Value) } + if (-not $ceq -and $ChildNode.Value -ine $Value) { $iList.Add($ChildNode.Value) } } - $Type = $String -as [Type] - if (-not $Type) { Throw "Unknown type: [$Object]" } - } - elseif ($Object -is [Type]) { - $Type = $Object.UnderlyingSystemType - if ("$Type" -eq 'string') { Return '' } - elseif ("$Type" -eq 'array') { Return ,@() } - elseif ("$Type" -eq 'scriptblock') { Return {} } + if ($ceq) { $this._Value = $cList -as $this._Value.GetType() } + else { $this._Value = $iList -as $this._Value.GetType() } } + $this.Cache.Remove('ChildNodes') + } + + RemoveAt([Int]$Index) { + if ($Index -lt 0 -or $Index -ge $this.Value.Count) { Throw 'Index was out of range. Must be non-negative and less than the size of the collection.' } + if ($this.Value.GetType().GetMethod('RemoveAt')) { $null = $this._Value.removeAt($Index) } else { - if ($Object -is [Object[]]) { Return ,@() } - elseif ($Object -is [ScriptBlock]) { Return {} } - elseif ($Object -is [PSCustomObject]) { Return [PSCustomObject]::new() } - $Type = $Object.GetType() + $this._Value = $(for ($i = 0; $i -lt $this._Value.Count; $i++) { + if ($i -ne $index) { $this._Value[$i] } + }) -as $this.ValueType } - try { return [Activator]::CreateInstance($Type) } catch { throw $_ } + $this.Cache.Remove('ChildNodes') } -} -Class PSKeyExpression { - hidden static [Regex]$UnquoteMatch = '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices - hidden $Key - hidden [PSLanguageMode]$LanguageMode = 'Restricted' - hidden [Bool]$Compress - hidden [Int]$MaxLength - - PSKeyExpression($Key) { $this.Key = $Key } - PSKeyExpression($Key, [PSLanguageMode]$LanguageMode) { $this.Key = $Key; $this.LanguageMode = $LanguageMode } - PSKeyExpression($Key, [PSLanguageMode]$LanguageMode, [Bool]$Compress) { $this.Key = $Key; $this.LanguageMode = $LanguageMode; $this.Compress = $Compress } - PSKeyExpression($Key, [int]$MaxLength) { $this.Key = $Key; $this.MaxLength = $MaxLength } - [String]ToString() { - $Name = $this.Key - if ($Name -is [byte] -or $Name -is [int16] -or $Name -is [int32] -or $Name -is [int64] -or - $Name -is [sByte] -or $Name -is [uint16] -or $Name -is [uint32] -or $Name -is [uint64] -or - $Name -is [float] -or $Name -is [double] -or $Name -is [decimal]) { return [Abbreviate]::new($Name, $this.MaxLength) - } - if ($this.MaxLength) { $Name = "$Name" } - if ($Name -is [String]) { - if ($Name -cMatch [PSKeyExpression]::UnquoteMatch) { return [Abbreviate]::new($Name, $this.MaxLength) } - return "'$([Abbreviate]::new($Name.Replace("'", "''"), ($this.MaxLength - 2)))'" + [Object]GetChildNode([Int]$Index) { + if ($this.MaxDepthReached()) { return @() } + $Count = $this._Value.get_Count() + if ($Index -lt -$Count -or $Index -ge $Count) { throw "The $($this.Path) doesn't contain a child index: $Index" } + if ($Index -lt 0) { $Index = $Count + $Index } # Negative index + if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = [Dictionary[Int,Object]]::new() } + if ( + -not $this.Cache.ChildNode.ContainsKey($Index) -or + -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Index]._Value, $this._Value[$Index]) + ) { + $Node = [PSNode]::ParseInput($this._Value[$Index]) + $Node._Name = $Index + $Node.Depth = $this.Depth + 1 + $Node.RootNode = [PSNode]$this.RootNode + $Node.ParentNode = $this + $this.Cache.ChildNode[$Index] = $Node } - $Node = [PSNode]::ParseInput($Name, 2) # There is no way to expand keys more than 2 levels - return [PSSerialize]::new($Node, $this.LanguageMode, -$this.Compress) - } -} -Class PSLanguageType { - hidden static $_TypeCache = [Dictionary[String,Bool]]::new() - hidden Static PSLanguageType() { # Hardcoded - [PSLanguageType]::_TypeCache['System.Void'] = $True - [PSLanguageType]::_TypeCache['System.Management.Automation.PSCustomObject'] = $True # https://github.com/PowerShell/PowerShell/issues/20767 + return $this.Cache.ChildNode[$Index] } - static [Bool]IsRestricted($TypeName) { - if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a restricted "type"! - $Type = $TypeName -as [Type] - if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } - $TypeName = $Type.FullName - return $TypeName -in 'bool', 'array', 'hashtable' + + hidden [Object[]]get_ChildNodes() { + if (-not $this.Cache.ContainsKey('ChildNodes')) { + $ChildNodes = for ($Index = 0; $Index -lt $this._Value.get_Count(); $Index++) { $this.GetChildNode($Index) } + if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } + } + return $this.Cache['ChildNodes'] } - static [Bool]IsConstrained($TypeName) { # https://stackoverflow.com/a/64806919/1701026 - if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a constrained "type"! - $Type = $TypeName -as [Type] - if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } - $TypeName = $Type.FullName - if (-not [PSLanguageType]::_TypeCache.ContainsKey($TypeName)) { - [PSLanguageType]::_TypeCache[$TypeName] = try { - $ConstrainedSession = [PowerShell]::Create() - $ConstrainedSession.RunSpace.SessionStateProxy.LanguageMode = 'Constrained' - $ConstrainedSession.AddScript("[$TypeName]0").Invoke().Count -ne 0 -or - $ConstrainedSession.Streams.Error[0].FullyQualifiedErrorId -ne 'ConversionSupportedOnlyToCoreTypes' - } catch { $False } + + [Int]GetHashCode($CaseSensitive) { + # The hash of a list node is equal if all items match the order and the case. + # The primary keys and the list type are not relevant + if ($null -eq $this._HashCode) { + $this._HashCode = [Dictionary[bool,int]]::new() + $this._ReferenceHashCode = [Dictionary[bool,int]]::new() } - return [PSLanguageType]::_TypeCache[$TypeName] + $ReferenceHashCode = $This._value.GetHashCode() + if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { + $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode + $HashCode = '@()'.GetHashCode() # Empty lists have a common hash that is not 0 + $Index = 0 + foreach ($Node in $this.GetNodeList()) { + $HashCode = $HashCode -bxor "$Index.$($Node.GetHashCode($CaseSensitive))".GetHashCode() + $index++ + } + $this._HashCode[$CaseSensitive] = $HashCode + } + return $this._HashCode[$CaseSensitive] } } -Class PSSerialize { - # hidden static [Dictionary[String,Bool]]$IsConstrainedType = [Dictionary[String,Bool]]::new() - hidden static [Dictionary[String,Bool]]$HasStringConstructor = [Dictionary[String,Bool]]::new() - - hidden static [String]$AnySingleQuote = "'|$([char]0x2018)|$([char]0x2019)" - - # NoLanguage mode only - hidden static [int]$MaxLeafLength = 48 - hidden static [int]$MaxKeyLength = 12 - hidden static [int]$MaxValueLength = 16 - hidden static [int[]]$NoLanguageIndices = 0, 1, -1 - hidden static [int[]]$NoLanguageItems = 0, 1, -1 - - hidden $_Object - - hidden [PSLanguageMode]$LanguageMode = 'Restricted' # "NoLanguage" will stringify the object for displaying (Use: PSStringify) - hidden [Int]$ExpandDepth = [Int]::MaxValue - hidden [Bool]$Explicit - hidden [Bool]$FullTypeName - hidden [bool]$HighFidelity - hidden [String]$Indent = ' ' - hidden [Bool]$ExpandSingleton - - # The dictionary below defines the round trip property. Unless the `-HighFidelity` switch is set, - # the serialization will stop (even it concerns a `PSCollectionNode`) when the specific property - # type is reached. - # * An empty string will return the string representation of the object: `""` - # * Any other string will return the string representation of the object property: `"$(.)"` - # * A ScriptBlock will be invoked and the result will be used for the object value +Class PSMapNode : PSCollectionNode { + hidden static PSMapNode() { Use-ClassAccessors } - hidden static $RoundTripProperty = @{ - 'Microsoft.Management.Infrastructure.CimInstance' = '' - 'Microsoft.Management.Infrastructure.CimSession' = 'ComputerName' - 'Microsoft.PowerShell.Commands.ModuleSpecification' = 'Name' - 'System.DateTime' = { $($Input).ToString('o') } - 'System.DirectoryServices.DirectoryEntry' = 'Path' - 'System.DirectoryServices.DirectorySearcher' = 'Filter' - 'System.Globalization.CultureInfo' = 'Name' - 'Microsoft.PowerShell.VistaCultureInfo' = 'Name' - 'System.Management.Automation.AliasAttribute' = 'AliasNames' - 'System.Management.Automation.ArgumentCompleterAttribute' = 'ScriptBlock' - 'System.Management.Automation.ConfirmImpact' = '' - 'System.Management.Automation.DSCResourceRunAsCredential' = '' - 'System.Management.Automation.ExperimentAction' = '' - 'System.Management.Automation.OutputTypeAttribute' = 'Type' - 'System.Management.Automation.PSCredential' = { ,@($($Input).UserName, @("(""$($($Input).Password | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)')) } - 'System.Management.Automation.PSListModifier' = 'Replace' - 'System.Management.Automation.PSReference' = 'Value' - 'System.Management.Automation.PSTypeNameAttribute' = 'PSTypeName' - 'System.Management.Automation.RemotingCapability' = '' - 'System.Management.Automation.ScriptBlock' = 'Ast' - 'System.Management.Automation.SemanticVersion' = '' - 'System.Management.Automation.ValidatePatternAttribute' = 'RegexPattern' - 'System.Management.Automation.ValidateScriptAttribute' = 'ScriptBlock' - 'System.Management.Automation.ValidateSetAttribute' = 'ValidValues' - 'System.Management.Automation.WildcardPattern' = { $($Input).ToWql().Replace('%', '*').Replace('_', '?').Replace('[*]', '%').Replace('[?]', '_') } - 'Microsoft.Management.Infrastructure.CimType' = '' - 'System.Management.ManagementClass' = 'Path' - 'System.Management.ManagementObject' = 'Path' - 'System.Management.ManagementObjectSearcher' = { $($Input).Query.QueryString } - 'System.Net.IPAddress' = 'IPAddressToString' - 'System.Net.IPEndPoint' = { $($Input).Address.Address; $($Input).Port } - 'System.Net.Mail.MailAddress' = 'Address' - 'System.Net.NetworkInformation.PhysicalAddress' = '' - 'System.Security.Cryptography.X509Certificates.X500DistinguishedName' = 'Name' - 'System.Security.SecureString' = { ,[string[]]("(""$($Input | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)') } - 'System.Text.RegularExpressions.Regex' = '' - 'System.RuntimeType' = '' - 'System.Uri' = 'OriginalString' - 'System.Version' = '' - 'System.Void' = $Null + [Int]GetHashCode($CaseSensitive) { + # The hash of a map node is equal if all names and items match the order and the case. + # The map type is not relevant + if ($null -eq $this._HashCode) { + $this._HashCode = [Dictionary[bool,int]]::new() + $this._ReferenceHashCode = [Dictionary[bool,int]]::new() + } + $ReferenceHashCode = $This._value.GetHashCode() + if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { + $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode + $HashCode = '@{}'.GetHashCode() # Empty maps have a common hash that is not 0 + $Index = 0 + foreach ($Node in $this.GetNodeList()) { + $Name = if ($CaseSensitive) { $Node._Name } else { $Node._Name.ToUpper() } + $HashCode = $HashCode -bxor "$Index.$Name=$($Node.GetHashCode())".GetHashCode() + $Index++ + } + $this._HashCode[$CaseSensitive] = $HashCode + } + return $this._HashCode[$CaseSensitive] } - hidden $StringBuilder - hidden [Int]$Offset = 0 - hidden [Int]$LineNumber = 1 +} +Class PSDictionaryNode : PSMapNode { + hidden static PSDictionaryNode() { Use-ClassAccessors } - PSSerialize($Object) { $this._Object = $Object } - PSSerialize($Object, $LanguageMode) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode + hidden PSDictionaryNode($Object) { + if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } } - PSSerialize($Object, $LanguageMode, $ExpandDepth) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode - $this.ExpandDepth = $ExpandDepth + + hidden [Object]get_Count() { + return $this._Value.get_Count() } - PSSerialize( - $Object, - $LanguageMode = 'Restricted', - $ExpandDepth = [Int]::MaxValue, - $Explicit = $False, - $FullTypeName = $False, - $HighFidelity = $False, - $ExpandSingleton = $False, - $Indent = ' ' - ) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode - $this.ExpandDepth = $ExpandDepth - $this.Explicit = $Explicit - $this.FullTypeName = $FullTypeName - $this.HighFidelity = $HighFidelity - $this.ExpandSingleton = $ExpandSingleton - $this.Indent = $Indent + + hidden [Object]get_Names() { + return ,$this._Value.get_Keys() } - hidden static [String[]]$Parameters = 'LanguageMode', 'Explicit', 'FullTypeName', 'HighFidelity', 'Indent', 'ExpandSingleton' - PSSerialize($Object, [HashTable]$Parameters) { - $this._Object = $Object - foreach ($Name in $Parameters.get_Keys()) { # https://github.com/PowerShell/PowerShell/issues/13307 - if ($Name -notin [PSSerialize]::Parameters) { Throw "Unknown parameter: $Name." } - $this.GetType().GetProperty($Name).SetValue($this, $Parameters[$Name]) - } + hidden [Object]get_Values() { + return ,$this._Value.get_Values() } - [String]Serialize($Object) { - if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } - if (-not ('ConstrainedLanguage', 'FullLanguage' -eq $this.LanguageMode)) { - if ($this.FullTypeName) { Write-Warning 'The FullTypeName switch requires Constrained - or FullLanguage mode.' } - if ($this.Explicit) { Write-Warning 'The Explicit switch requires Constrained - or FullLanguage mode.' } + hidden [Object]get_CaseMatters() { #Returns Nullable[Boolean] + if (-not $this.Cache.ContainsKey('CaseMatters')) { + $this.Cache['CaseMatters'] = $null # else $Null means that there is no key with alphabetic characters in the dictionary + foreach ($Key in $this._Value.Get_Keys()) { + if ($Key -is [String] -and $Key -match '[a-z]') { + $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } + $this.Cache['CaseMatters'] = -not $this.Contains($Case) -or $Case -cin $this._Value.Get_Keys() + break + } + } } - if ($Object -is [PSNode]) { $Node = $Object } else { $Node = [PSNode]::ParseInput($Object) } - $this.StringBuilder = [System.Text.StringBuilder]::new() - $this.Stringify($Node) - return $this.StringBuilder.ToString() + return $this.Cache['CaseMatters'] } - hidden Stringify([PSNode]$Node) { - $Value = $Node.Value - $IsSubNode = $this.StringBuilder.Length -ne 0 - if ($Null -eq $Value) { - $this.StringBuilder.Append('$Null') - return + [Bool]Contains($Key) { + if ($this._Value.GetType().GetMethod('ContainsKey')) { + return $this._Value.ContainsKey($Key) } - $Type = $Node.ValueType - $TypeName = "$Type" - $TypeInitializer = - if ($Null -ne $Type -and ( - $this.LanguageMode -eq 'Full' -or ( - $this.LanguageMode -eq 'Constrained' -and - [PSLanguageType]::IsConstrained($Type) -and ( - $this.Explicit -or -not ( - $Type.IsPrimitive -or - $Value -is [String] -or - $Value -is [Object[]] -or - $Value -is [Hashtable] - ) - ) - ) - ) - ) { - if ($this.FullTypeName) { - if ($Type.FullName -eq 'System.Management.Automation.PSCustomObject' ) { '[System.Management.Automation.PSObject]' } # https://github.com/PowerShell/PowerShell/issues/2295 - else { "[$($Type.FullName)]" } - } - elseif ($TypeName -eq 'System.Object[]') { "[Array]" } - elseif ($TypeName -eq 'System.Management.Automation.PSCustomObject') { "[PSCustomObject]" } - elseif ($Type.Name -eq 'RuntimeType') { "[Type]" } - else { "[$TypeName]" } - } - if ($TypeInitializer) { $this.StringBuilder.Append($TypeInitializer) } + else { + return $this._Value.Contains($Key) + } + } + [Bool]Exists($Key) { return $this.Contains($Key) } - if ($Node -is [PSLeafNode] -or (-not $this.HighFidelity -and [PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName))) { - $MaxLength = if ($IsSubNode) { [PSSerialize]::MaxValueLength } else { [PSSerialize]::MaxLeafLength } + [Object]GetValue($Key) { return $this._Value[$Key] } + [Object]GetValue($Key, $Default) { + if (-not $This.Contains($Key)) { return $Default } + return $this._Value[$Key] + } - if ([PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName)) { - $Property = [PSSerialize]::RoundTripProperty[$Node.ValueType.FullName] - if ($Null -eq $Property) { $Expression = $Null } - elseif ($Property -is [String]) { $Expression = if ($Property) { ,$Value.$Property } else { "$Value" } } - elseif ($Property -is [ScriptBlock] ) { $Expression = Invoke-Command $Property -InputObject $Value } - elseif ($Property -is [HashTable]) { $Expression = if ($this.LanguageMode -eq 'Restricted') { $Null } else { @{} } } - elseif ($Property -is [Array]) { $Expression = @($Property.foreach{ $Value.$_ }) } - else { Throw "Unknown round trip property type: $($Property.GetType())."} - } - elseif ($Value -is [Type]) { $Expression = @() } - elseif ($Value -is [Attribute]) { $Expression = @() } - elseif ($Type.IsPrimitive) { $Expression = $Value } - elseif (-not $Type.GetConstructors()) { $Expression = "$TypeName" } - elseif ($Type.GetMethod('ToString', [Type[]]@())) { $Expression = $Value.ToString() } - elseif ($Value -is [Collections.ICollection]) { $Expression = ,$Value } - else { $Expression = $Value } # Handle compression + SetValue($Key, $Value) { + if ($Value -is [PSNode]) { $Value = $Value.Value } + $this._Value[$Key] = $Value + $this.Cache.Remove('ChildNodes') + } - if ($Null -eq $Expression) { $Expression = '$Null' } - elseif ($Expression -is [Bool]) { $Expression = "`$$Value" } - elseif ($Expression -is [Char]) { $Expression = "'$Value'" } - elseif ($Expression -is [ScriptBlock]) { $Expression = [Abbreviate]::new('{', $Expression, $MaxLength, '}') } - elseif ($Expression -is [HashTable]) { $Expression = '@{}' } - elseif ($Expression -is [Array]) { - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [Abbreviate]::new('[', $Expression[0], $MaxLength, ']') } - else { - $Space = if ($this.ExpandDepth -ge 0) { ' ' } - $New = if ($TypeInitializer) { '::new(' } else { '@(' } - $Expression = $New + ($Expression.foreach{ - if ($Null -eq $_) { '$Null' } - elseif ($_.GetType().IsPrimitive) { "$_" } - elseif ($_ -is [Array]) { $_ -Join $Space } - else { "'$_'" } - } -Join ",$Space") + ')' - } - } - elseif ($Type -and $Type.IsPrimitive) { - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [CommandColor]([String]$Expression[0]) } - } - else { - if ($Expression -isnot [String]) { $Expression = "$Expression" } - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [StringColor]([Abbreviate]::new("'", $Expression, $MaxLength, "'")) } - else { - if ($Expression.Contains("`n")) { - $Expression = "@'" + [Environment]::NewLine + "$Expression".Replace("'", "''") + [Environment]::NewLine + "'@" - } - else { $Expression = "'$($Expression -Replace [PSSerialize]::AnySingleQuote, '$0$0')'" } - } - } + Add($Key, $Value) { + if ($this.Contains($Key)) { Throw "Item '$Key' has already been added." } + if ($Value -is [PSNode]) { $Value = $Value.Value } + $this._Value.Add($Key, $Value) + $this.Cache.Remove('ChildNodes') + } - $this.StringBuilder.Append($Expression) - } - elseif ($Node -is [PSListNode]) { - $ChildNodes = $Node.get_ChildNodes() - $this.StringBuilder.Append('@(') - if ($this.LanguageMode -eq 'NoLanguage') { - if ($ChildNodes.Count -eq 0) { } - elseif ($IsSubNode) { $this.StringBuilder.Append([Abbreviate]::Ellipses) } - else { - $Indices = [PSSerialize]::NoLanguageIndices - if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } - $LastIndex = $Null - foreach ($Index in $Indices) { - if ($Null -ne $LastIndex) { $this.StringBuilder.Append(',') } - if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } - if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses),") } - $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) - $LastIndex = $Index - } - } - } - else { - $this.Offset++ - $StartLine = $this.LineNumber - $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -isnot [PSLeafNode]) - foreach ($ChildNode in $ChildNodes) { - if ($ChildNode.Name -gt 0) { - $this.StringBuilder.Append(',') - $this.NewWord() - } - elseif ($ExpandSingle) { $this.NewWord('') } - $this.Stringify($ChildNode) - } - $this.Offset-- - if ($this.LineNumber -gt $StartLine) { $this.NewWord('') } - } - $this.StringBuilder.Append(')') - } - else { # if ($Node -is [PSMapNode]) { - $ChildNodes = $Node.get_ChildNodes() - if ($ChildNodes) { - $this.StringBuilder.Append('@{') - if ($this.LanguageMode -eq 'NoLanguage') { - if ($ChildNodes.Count -gt 0) { - $Indices = [PSSerialize]::NoLanguageItems - if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } - $LastIndex = $Null - foreach ($Index in $Indices) { - if ($IsSubNode -and $Index) { $this.StringBuilder.Append(";$([Abbreviate]::Ellipses)"); break } - if ($Null -ne $LastIndex) { $this.StringBuilder.Append(';') } - if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } - if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses);") } - $this.StringBuilder.Append([VariableColor]( - [PSKeyExpression]::new($ChildNodes[$Index].Name, [PSSerialize]::MaxKeyLength))) - $this.StringBuilder.Append('=') - if (-not $IsSubNode -or $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength) { - $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) - } - else { $this.StringBuilder.Append([Abbreviate]::Ellipses) } - $LastIndex = $Index - } - } - } - else { - $this.Offset++ - $StartLine = $this.LineNumber - $Index = 0 - $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or $ChildNodes[0] -isnot [PSLeafNode] - foreach ($ChildNode in $ChildNodes) { - if ($ChildNode.Name -eq 'TypeId' -and $Node._Value -is $ChildNode._Value) { continue } - if ($Index++) { - $Separator = if ($this.ExpandDepth -ge 0) { '; ' } else { ';' } - $this.NewWord($Separator) - } - elseif ($this.ExpandDepth -ge 0) { - if ($ExpandSingle) { $this.NewWord() } else { $this.StringBuilder.Append(' ') } - } - $this.StringBuilder.Append([PSKeyExpression]::new($ChildNode.Name, $this.LanguageMode, ($this.ExpandDepth -lt 0))) - if ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' = ') } else { $this.StringBuilder.Append('=') } - $this.Stringify($ChildNode) + Remove($Key) { + $null = $this._Value.Remove($Key) + $this.Cache.Remove('ChildNodes') + } + + hidden RemoveAt($Key) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) } + if (-not $this.Contains($Key)) { Throw "Item '$Key' doesn't exist." } + $null = $this._Value.Remove($Key) + $this.Cache.Remove('ChildNodes') + } + + [Object]GetChildNode([Object]$Key) { + if ($this.MaxDepthReached()) { return @() } + if (-not $this.Contains($Key)) { Throw "The $($this.Path) doesn't contain a child named: $Key" } + if (-not $this.Cache.ContainsKey('ChildNode')) { + # The ChildNode cache case sensitivity is based on the current dictionary population. + # The ChildNode cache is always ordinal, if the contained dictionary is invariant, extra entries might + # appear in the cache but shouldn't effect the results other than slightly slow down the performance. + # In other words, do not use the cache to count the entries. Custom comparers are not supported. + $this.Cache['ChildNode'] = if ($this.get_CaseMatters()) { [HashTable]::new() } else { @{} } # default is case insensitive + } + elseif ( + -not $this.Cache.ChildNode.ContainsKey($Key) -or + -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key]._Value, $this._Value[$Key]) + ) { + if($null -eq $this.get_CaseMatters()) { # If the case was undetermined, check the new key for case sensitivity + $this.Cache.CaseMatters = if ($Key -is [String] -and $Key -match '[a-z]') { + $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } + -not $this._Value.Contains($Case) -or $Case -cin $this._Value.Get_Keys() + } + if ($this.get_CaseMatters()) { + $ChildNode = $this.Cache['ChildNode'] + $this.Cache['ChildNode'] = [HashTable]::new() # Create a new cache as it appears to be case sensitive + foreach ($Name in $ChildNode.get_Keys()) { # Migrate the content + $this.Cache.ChildNode[$Name] = $ChildNode[$Name] } - $this.Offset-- - if ($this.LineNumber -gt $StartLine) { $this.NewWord() } - elseif ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' ') } } - $this.StringBuilder.Append('}') } - elseif ($Node -is [PSObjectNode] -and $TypeInitializer) { $this.StringBuilder.Append('::new()') } - else { $this.StringBuilder.Append('@{}') } } + if ( + -not $this.Cache.ChildNode.ContainsKey($Key) -or + -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key].Value, $this._Value[$Key]) + ) { + $Node = [PSNode]::ParseInput($this._Value[$Key]) + $Node._Name = $Key + $Node.Depth = $this.Depth + 1 + $Node.RootNode = [PSNode]$this.RootNode + $Node.ParentNode = $this + $this.Cache.ChildNode[$Key] = $Node + } + return $this.Cache.ChildNode[$Key] } - hidden NewWord() { $this.NewWord(' ') } - hidden NewWord([String]$Separator) { - if ($this.Offset -le $this.ExpandDepth) { - $this.StringBuilder.AppendLine() - for($i = $this.Offset; $i -gt 0; $i--) { - $this.StringBuilder.Append($this.Indent) - } - $this.LineNumber++ - } - else { - $this.StringBuilder.Append($Separator) + hidden [Object[]]get_ChildNodes() { + if (-not $this.Cache.ContainsKey('ChildNodes')) { + $ChildNodes = foreach ($Key in $this._Value.get_Keys()) { $this.GetChildNode($Key) } + if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } } + return $this.Cache['ChildNodes'] } +} +Class PSObjectNode : PSMapNode { + hidden static PSObjectNode() { Use-ClassAccessors } - [String] ToString() { - if ($this._Object -is [PSNode]) { $Node = $this._Object } - else { $Node = [PSNode]::ParseInput($this._Object) } - $this.StringBuilder = [System.Text.StringBuilder]::new() - $this.Stringify($Node) - return $this.StringBuilder.ToString() + hidden PSObjectNode($Object) { + if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } } -} -Class ANSI { - # Retrieved from Get-PSReadLineOption - static [String]$CommandColor - static [String]$CommentColor - static [String]$ContinuationPromptColor - static [String]$DefaultTokenColor - static [String]$EmphasisColor - static [String]$ErrorColor - static [String]$KeywordColor - static [String]$MemberColor - static [String]$NumberColor - static [String]$OperatorColor - static [String]$ParameterColor - static [String]$SelectionColor - static [String]$StringColor - static [String]$TypeColor - static [String]$VariableColor - # Hardcoded (if valid Get-PSReadLineOption) - static [String]$Reset - static [String]$ResetColor - static [String]$InverseColor - static [String]$InverseOff + hidden [Object]get_Count() { + return @($this._Value.PSObject.Properties).get_Count() + } - Static ANSI() { - $PSReadLineOption = try { Get-PSReadLineOption -ErrorAction SilentlyContinue } catch { $null } - if (-not $PSReadLineOption) { return } - $ANSIType = [ANSI] -as [Type] - foreach ($Property in [ANSI].GetProperties()) { - $PSReadLineProperty = $PSReadLineOption.PSObject.Properties[$Property.Name] - if ($PSReadLineProperty) { - $ANSIType.GetProperty($Property.Name).SetValue($Property.Name, $PSReadLineProperty.Value) - } - } - $Esc = [char]0x1b - [ANSI]::Reset = "$Esc[0m" - [ANSI]::ResetColor = "$Esc[39m" - [ANSI]::InverseColor = "$Esc[7m" - [ANSI]::InverseOff = "$Esc[27m" + hidden [Object]get_Names() { + return ,$this._Value.PSObject.Properties.Name } -} -Class TextStyle { - hidden [String]$Text - hidden [String]$AnsiCode - hidden [String]$ResetCode = [ANSI]::Reset - TextStyle ([String]$Text, [String]$AnsiCode, [String]$ResetCode) { - $this.Text = $Text - $this.AnsiCode = $AnsiCode - $this.ResetCode = $ResetCode + + hidden [Object]get_Values() { + return ,$this._Value.PSObject.Properties.Value } - TextStyle ([String]$Text, [String]$AnsiCode) { - $this.Text = $Text - $this.AnsiCode = $AnsiCode + + hidden [Object]get_CaseMatters() { return $false } + + [Bool]Contains($Name) { + return $this._Value.PSObject.Properties[$Name] } - [String] ToString() { - if ($this.ResetCode -eq [ANSI]::ResetColor) { - return "$($this.AnsiCode)$($this.Text.Replace($this.ResetCode, $this.AnsiCode))$($this.ResetCode)" - } - else { - return "$($this.AnsiCode)$($this.Text)$($this.ResetCode)" - } + + [Bool]Exists($Name) { + return $this._Value.PSObject.Properties[$Name] } -} -class XdnName { - hidden [Bool]$_Literal - hidden $_IsVerbatim - hidden $_ContainsWildcard - hidden $_Value - hidden Initialize($Value, $Literal) { - $this._Value = $Value - if ($Null -ne $Literal) { $this._Literal = $Literal } else { $this._Literal = $this.IsVerbatim() } - if ($this._Literal) { - $XdnName = [XdnName]::new() - $XdnName._ContainsWildcard = $False - } + [Object]GetValue($Name) { return $this._Value.PSObject.Properties[$Name].Value } + [Object]GetValue($Name, $Default) { + if (-not $this.Contains($Name)) { return $Default } + return $this._Value[$Name] + } + + SetValue($Name, $Value) { + if ($Value -is [PSNode]) { $Value = $Value.Value } + if ($this._Value -isnot [PSCustomObject]) { + $Properties = [Ordered]@{} + foreach ($Property in $this._Value.PSObject.Properties) { $Properties[$Property.Name] = $Property.Value } + $Properties[$Name] = $Value + $this._Value = [PSCustomObject]$Properties + $this.Cache.Remove('ChildNodes') + } + elseif ($this._Value.PSObject.Properties[$Name]) { + $this._Value.PSObject.Properties[$Name].Value = $Value + } else { - $XdnName = [XdnName]::new() - $XdnName._ContainsWildcard = $null + $this._Value.PSObject.Properties.Add([PSNoteProperty]::new($Name, $Value)) + $this.Cache.Remove('ChildNodes') } + } + Add($Name, $Value) { + if ($this.Contains($Name)) { Throw "Item '$Name' has already been added." } + $this.SetValue($Name, $Value) } - XdnName() {} - XdnName($Value) { $this.Initialize($Value, $null) } - XdnName($Value, [Bool]$Literal) { $this.Initialize($Value, $Literal) } - static [XdnName]Literal($Value) { return [XdnName]::new($Value, $true) } - static [XdnName]Expression($Value) { return [XdnName]::new($Value, $false) } - [Bool] IsVerbatim() { - if ($Null -eq $this._IsVerbatim) { - $this._IsVerbatim = $this._Value -is [String] -and $this._Value -Match '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices - } - return $this._IsVerbatim + Remove($Name) { + $this._Value.PSObject.Properties.Remove($Name) + $this.Cache.Remove('ChildNodes') } - [Bool] ContainsWildcard() { - if ($Null -eq $this._ContainsWildcard) { - $this._ContainsWildcard = $this._Value -is [String] -and $this._Value -Match '(?<=([^`]|^)(``)*)[\?\*]' - } - return $this._ContainsWildcard + hidden RemoveAt($Name) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) } + if (-not $this.Contains($Name)) { Throw "Item '$Name' doesn't exist." } + $this._Value.PSObject.Properties.Remove($Name) + $this.Cache.Remove('ChildNodes') } - [Bool] Equals($Object) { - if ($this._Literal) { return $this._Value -eq $Object } - elseif ($this.ContainsWildcard()) { return $Object -Like $this._Value } - else { return $this._Value -eq $Object } + [Object]GetChildNode([String]$Name) { + if ($this.MaxDepthReached()) { return @() } + if (-not $this.Contains($Name)) { Throw Throw "$($this.GetPathName('')) doesn't contain a child named: $Name" } + if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = @{} } # Object properties are case insensitive + if ( + -not $this.Cache.ChildNode.ContainsKey($Name) -or + -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Name]._Value, $this._Value.PSObject.Properties[$Name].Value) + ) { + $Node = [PSNode]::ParseInput($this._Value.PSObject.Properties[$Name].Value) + $Node._Name = $Name + $Node.Depth = $this.Depth + 1 + $Node.RootNode = [PSNode]$this.RootNode + $Node.ParentNode = $this + $this.Cache.ChildNode[$Name] = $Node + } + return $this.Cache.ChildNode[$Name] } - [String] ToString($Colored) { - $Color = if ($Colored) { - if ($this._Literal) { [ANSI]::VariableColor } - elseif (-not $this.IsVerbatim()) { [ANSI]::StringColor } - elseif ($this.ContainsWildcard()) { [ANSI]::EmphasisColor } - else { [ANSI]::VariableColor } + hidden [Object[]]get_ChildNodes() { + if (-not $this.Cache.ContainsKey('ChildNodes')) { + $ChildNodes = foreach ($Property in $this._Value.PSObject.Properties) { $this.GetChildNode($Property.Name) } + if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } } - $String = - if ($this._Literal) { "'" + "$($this._Value)".Replace("'", "''") + "'" } - else { "$($this._Value)" -replace '(?$($Node.Path)" - } - - hidden [List[Ast]] GetAstSelectors ($Ast) { - $List = [List[Ast]]::new() - if ($Ast -isnot [Ast]) { - $Ast = [Parser]::ParseInput("`$_$Ast", [ref]$Null, [ref]$Null) - $Ast = $Ast.EndBlock.Statements.PipeLineElements.Expression + elseif ($Ast -is [PipelineAst]) { + $Elements = $Ast.PipelineElements + if (-not $Elements.Count) { return @() } + elseif ($Elements -is [CommandAst]) { + return $Null #85 ConvertFrom-Expression: convert function/cmdlet calls to Objects + } + elseif ($Elements.Expression.Count -eq 1) { return $this.ParseAst($Elements.Expression[0]) } + else { return $Elements.Expression.Foreach{ $this.ParseAst($_) } } } - if ($Ast -is [IndexExpressionAst]) { - $List.AddRange($this.GetAstSelectors($Ast.Target)) - $List.Add($Ast) + elseif ($Ast -is [ArrayLiteralAst] -or $Ast -is [ArrayExpressionAst]) { + if (-not $Type -or 'System.Object[]', 'System.Array' -eq $Type.FullName) { $Type = $this.ArrayType } + if ($Ast -is [ArrayLiteralAst]) { $Value = $Ast.Elements.foreach{ $this.ParseAst($_) } } + else { $Value = $Ast.SubExpression.Statements.foreach{ $this.ParseAst($_) } } + if ('System.Object[]', 'System.Array' -eq $Type.FullName) { + if ($Value -isnot [Array]) { $Value = @($Value) } # Prevent single item array unrolls + } + else { $Value = $Value -as $Type } + return $Value } - elseif ($Ast -is [MemberExpressionAst]) { - $List.AddRange($this.GetAstSelectors($Ast.Expression)) - $List.Add($Ast) + elseif ($Ast -is [HashtableAst]) { + if (-not $Type -or $Type.FullName -eq 'System.Collections.Hashtable') { $Type = $this.HashTableType } + $IsPSCustomObject = "$Type" -in + 'PSCustomObject', + 'System.Management.Automation.PSCustomObject', + 'PSObject', + 'System.Management.Automation.PSObject' + if ($Type.FullName -eq 'System.Collections.Hashtable') { $Map = @{} } # Case insensitive + elseif ($IsPSCustomObject) { $Map = [Ordered]@{} } + else { $Map = New-Object -Type $Type } + $Ast.KeyValuePairs.foreach{ + if ( $Map -is [Collections.IDictionary]) { $Map.Add($_.Item1.Value, $this.ParseAst($_.Item2)) } + else { $Map."$($_.Item1.Value)" = $this.ParseAst($_.Item2) } + } + if ($IsPSCustomObject) { return [PSCustomObject]$Map } else { return $Map } } - elseif ($Ast.Extent.Text -ne '$_') { - Throw "Parse error: $($Ast.Extent.Text)" + elseif ($Ast -is [ConstantExpressionAst]) { + if ($Type) { $Value = $Ast.Value -as $Type } else { $Value = $Ast.Value } + return $Value } - return $List - } - - [List[PSNode]]GetNodeList($Levels, [Bool]$LeafNodesOnly) { - $NodeList = [List[PSNode]]::new() - $Stack = [Stack]::new() - $Stack.Push($this.get_ChildNodes().GetEnumerator()) - $Level = 1 - While ($Stack.Count -gt 0) { - $Enumerator = $Stack.Pop() - $Level-- - while ($Enumerator.MoveNext()) { - $Node = $Enumerator.Current - if ($Node.MaxDepthReached() -or ($Levels -ge 0 -and $Level -ge $Levels)) { break } - if (-not $LeafNodesOnly -or $Node -is [PSLeafNode]) { $NodeList.Add($Node) } - if ($Node -is [PSCollectionNode]) { - $Stack.Push($Enumerator) - $Level++ - $Enumerator = $Node.get_ChildNodes().GetEnumerator() - } + elseif ($Ast -is [VariableExpressionAst]) { + $Value = switch ($Ast.VariablePath.UserPath) { + Null { $Null } + True { $True } + False { $False } + PSCulture { (Get-Culture).ToString() } + PSUICulture { (Get-UICulture).ToString() } + Default { $Ast.Extent.Text } } + return $Value } - return $NodeList - } - [List[PSNode]]GetNodeList() { return $this.GetNodeList(1, $False) } - [List[PSNode]]GetNodeList([Int]$Levels) { return $this.GetNodeList($Levels, $False) } - hidden [PSNode[]]get_DescendantNodes() { return $this.GetNodeList(-1, $False) } - hidden [PSNode[]]get_LeafNodes() { return $this.GetNodeList(-1, $True) } - - Sort() { $this.Sort($Null, 0) } - Sort([ObjectComparison]$ObjectComparison) { $this.Sort($Null, $ObjectComparison) } - Sort([String[]]$PrimaryKey) { $this.Sort($PrimaryKey, 0) } - Sort([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.Sort($PrimaryKey, $ObjectComparison) } - Sort([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { - # As the child nodes are sorted first, we just do a side-by-side node compare: - $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' - $ObjectComparison = $ObjectComparison -band (-1 - [ObjectComparison]'IgnoreListOrder') - $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $this.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) + else { return $Null } } - - hidden SortRecurse([PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { - $NodeList = $this.GetNodeList() - foreach ($Node in $NodeList) { - if ($Node -is [PSCollectionNode]) { $Node.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) } +} +Class PSInstance { + static [Object]Create($Object) { + if ($Null -eq $Object) { return $Null } + elseif ($Object -is [String]) { + $String = if ($Object.StartsWith('[') -and $Object.EndsWith(']')) { $Object.SubString(1, ($Object.Length - 2)) } else { $Object } + Switch -Regex ($String) { + '^((System\.)?String)?$' { return '' } + '^(System\.)?Array$' { return ,@() } + '^(System\.)?Object\[\]$' { return ,@() } + '^((System\.)?Collections\.Hashtable\.)?hashtable$' { return @{} } + '^((System\.)?Management\.Automation\.)?ScriptBlock$' { return {} } + '^((System\.)?Collections\.Specialized\.)?Ordered(Dictionary)?$' { return [Ordered]@{} } + '^((System\.)?Management\.Automation\.)?PS(Custom)?Object$' { return [PSCustomObject]@{} } + } + $Type = $String -as [Type] + if (-not $Type) { Throw "Unknown type: [$Object]" } } - if ($this -is [PSListNode]) { - $NodeList.Sort($PSListNodeComparer) - if ($NodeList.Count) { $this._Value = @($NodeList.Value) } else { $this._Value = @() } + elseif ($Object -is [Type]) { + $Type = $Object.UnderlyingSystemType + if ("$Type" -eq 'string') { Return '' } + elseif ("$Type" -eq 'array') { Return ,@() } + elseif ("$Type" -eq 'scriptblock') { Return {} } } - else { # if ($Node -is [PSMapNode]) - $NodeList.Sort($PSMapNodeComparer) - $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) - foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) - if ($this -is [PSObjectNode]) { $this._Value = [PSCustomObject]$Properties } else { $this._Value = $Properties } + else { + if ($Object -is [Object[]]) { Return ,@() } + elseif ($Object -is [ScriptBlock]) { Return {} } + elseif ($Object -is [PSCustomObject]) { Return [PSCustomObject]::new() } + $Type = $Object.GetType() } + try { return [Activator]::CreateInstance($Type) } catch { throw $_ } } } -Class PSListNode : PSCollectionNode { - hidden static PSListNode() { Use-ClassAccessors } +Class PSKeyExpression { + hidden static [Regex]$UnquoteMatch = '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices + hidden $Key + hidden [PSLanguageMode]$LanguageMode = 'Restricted' + hidden [Bool]$Compress + hidden [Int]$MaxLength - hidden PSListNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } + PSKeyExpression($Key) { $this.Key = $Key } + PSKeyExpression($Key, [PSLanguageMode]$LanguageMode) { $this.Key = $Key; $this.LanguageMode = $LanguageMode } + PSKeyExpression($Key, [PSLanguageMode]$LanguageMode, [Bool]$Compress) { $this.Key = $Key; $this.LanguageMode = $LanguageMode; $this.Compress = $Compress } + PSKeyExpression($Key, [int]$MaxLength) { $this.Key = $Key; $this.MaxLength = $MaxLength } + + [String]ToString() { + $Name = $this.Key + if ($Name -is [byte] -or $Name -is [int16] -or $Name -is [int32] -or $Name -is [int64] -or + $Name -is [sByte] -or $Name -is [uint16] -or $Name -is [uint32] -or $Name -is [uint64] -or + $Name -is [float] -or $Name -is [double] -or $Name -is [decimal]) { return [Abbreviate]::new($Name, $this.MaxLength) + } + if ($this.MaxLength) { $Name = "$Name" } + if ($Name -is [String]) { + if ($Name -cMatch [PSKeyExpression]::UnquoteMatch) { return [Abbreviate]::new($Name, $this.MaxLength) } + return "'$([Abbreviate]::new($Name.Replace("'", "''"), ($this.MaxLength - 2)))'" + } + $Node = [PSNode]::ParseInput($Name, 2) # There is no way to expand keys more than 2 levels + return [PSSerialize]::new($Node, $this.LanguageMode, -$this.Compress) } - - hidden [Object]get_Count() { - return $this._Value.get_Count() +} +Class PSLanguageType { + hidden static $_TypeCache = [Dictionary[String,Bool]]::new() + hidden Static PSLanguageType() { # Hardcoded + [PSLanguageType]::_TypeCache['System.Void'] = $True + [PSLanguageType]::_TypeCache['System.Management.Automation.PSCustomObject'] = $True # https://github.com/PowerShell/PowerShell/issues/20767 } - - hidden [Object]get_Names() { - if ($this._Value.Length) { return ,@(0..($this._Value.Length - 1)) } - return ,@() + static [Bool]IsRestricted($TypeName) { + if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a restricted "type"! + $Type = $TypeName -as [Type] + if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } + $TypeName = $Type.FullName + return $TypeName -in 'bool', 'array', 'hashtable' } - - hidden [Object]get_Values() { - return ,@($this._Value) + static [Bool]IsConstrained($TypeName) { # https://stackoverflow.com/a/64806919/1701026 + if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a constrained "type"! + $Type = $TypeName -as [Type] + if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } + $TypeName = $Type.FullName + if (-not [PSLanguageType]::_TypeCache.ContainsKey($TypeName)) { + [PSLanguageType]::_TypeCache[$TypeName] = try { + $ConstrainedSession = [PowerShell]::Create() + $ConstrainedSession.RunSpace.SessionStateProxy.LanguageMode = 'Constrained' + $ConstrainedSession.AddScript("[$TypeName]0").Invoke().Count -ne 0 -or + $ConstrainedSession.Streams.Error[0].FullyQualifiedErrorId -ne 'ConversionSupportedOnlyToCoreTypes' + } catch { $False } + } + return [PSLanguageType]::_TypeCache[$TypeName] } +} +Class PSSerialize { + # hidden static [Dictionary[String,Bool]]$IsConstrainedType = [Dictionary[String,Bool]]::new() + hidden static [Dictionary[String,Bool]]$HasStringConstructor = [Dictionary[String,Bool]]::new() - hidden [Object]get_CaseMatters() { return $false } - - [Bool]Contains($Index) { - return $Index -ge 0 -and $Index -lt $this.get_Count() - } + hidden static [String]$AnySingleQuote = "'|$([char]0x2018)|$([char]0x2019)" - [Bool]Exists($Index) { - return $Index -ge 0 -and $Index -lt $this.get_Count() - } + # NoLanguage mode only + hidden static [int]$MaxLeafLength = 48 + hidden static [int]$MaxKeyLength = 12 + hidden static [int]$MaxValueLength = 16 + hidden static [int[]]$NoLanguageIndices = 0, 1, -1 + hidden static [int[]]$NoLanguageItems = 0, 1, -1 - [Object]GetValue($Index) { return $this._Value[$Index] } - [Object]GetValue($Index, $Default) { - if (-not $This.Contains($Index)) { return $Default } - return $this._Value[$Index] - } + hidden $_Object - SetValue($Index, $Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value[$Index] = $Value - } + hidden [PSLanguageMode]$LanguageMode = 'Restricted' # "NoLanguage" will stringify the object for displaying (Use: PSStringify) + hidden [Int]$ExpandDepth = [Int]::MaxValue + hidden [Bool]$Explicit + hidden [Bool]$FullTypeName + hidden [bool]$HighFidelity + hidden [String]$Indent = ' ' + hidden [Bool]$ExpandSingleton - Add($Value) { - if ($Value -is [PSNode]) { $Value = $Value._Value } - if ($this._Value.GetType().GetMethod('Add')) { $null = $This._Value.Add($Value) } - else { $this._Value = ($this._Value + $Value) -as $this._Value.GetType() } - $this.Cache.Remove('ChildNodes') - } + # The dictionary below defines the round trip property. Unless the `-HighFidelity` switch is set, + # the serialization will stop (even it concerns a `PSCollectionNode`) when the specific property + # type is reached. + # * An empty string will return the string representation of the object: `""` + # * Any other string will return the string representation of the object property: `"$(.)"` + # * A ScriptBlock will be invoked and the result will be used for the object value - Remove($Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - if (-not $this.Value.Contains($Value)) { return } - if ($this.Value.GetType().GetMethod('Remove')) { $null = $this._value.remove($Value) } - else { - $cList = [List[Object]]::new() - $iList = [List[Object]]::new() - $ceq = $false - foreach ($ChildNode in $this.get_ChildNodes()) { - if (-not $ceq -and $ChildNode.Value -ceq $Value) { $ceq = $true } else { $cList.Add($ChildNode.Value) } - if (-not $ceq -and $ChildNode.Value -ine $Value) { $iList.Add($ChildNode.Value) } - } - if ($ceq) { $this._Value = $cList -as $this._Value.GetType() } - else { $this._Value = $iList -as $this._Value.GetType() } - } - $this.Cache.Remove('ChildNodes') + hidden static $RoundTripProperty = @{ + 'Microsoft.Management.Infrastructure.CimInstance' = '' + 'Microsoft.Management.Infrastructure.CimSession' = 'ComputerName' + 'Microsoft.PowerShell.Commands.ModuleSpecification' = 'Name' + 'System.DateTime' = { $($Input).ToString('o') } + 'System.DirectoryServices.DirectoryEntry' = 'Path' + 'System.DirectoryServices.DirectorySearcher' = 'Filter' + 'System.Globalization.CultureInfo' = 'Name' + 'Microsoft.PowerShell.VistaCultureInfo' = 'Name' + 'System.Management.Automation.AliasAttribute' = 'AliasNames' + 'System.Management.Automation.ArgumentCompleterAttribute' = 'ScriptBlock' + 'System.Management.Automation.ConfirmImpact' = '' + 'System.Management.Automation.DSCResourceRunAsCredential' = '' + 'System.Management.Automation.ExperimentAction' = '' + 'System.Management.Automation.OutputTypeAttribute' = 'Type' + 'System.Management.Automation.PSCredential' = { ,@($($Input).UserName, @("(""$($($Input).Password | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)')) } + 'System.Management.Automation.PSListModifier' = 'Replace' + 'System.Management.Automation.PSReference' = 'Value' + 'System.Management.Automation.PSTypeNameAttribute' = 'PSTypeName' + 'System.Management.Automation.RemotingCapability' = '' + 'System.Management.Automation.ScriptBlock' = 'Ast' + 'System.Management.Automation.SemanticVersion' = '' + 'System.Management.Automation.ValidatePatternAttribute' = 'RegexPattern' + 'System.Management.Automation.ValidateScriptAttribute' = 'ScriptBlock' + 'System.Management.Automation.ValidateSetAttribute' = 'ValidValues' + 'System.Management.Automation.WildcardPattern' = { $($Input).ToWql().Replace('%', '*').Replace('_', '?').Replace('[*]', '%').Replace('[?]', '_') } + 'Microsoft.Management.Infrastructure.CimType' = '' + 'System.Management.ManagementClass' = 'Path' + 'System.Management.ManagementObject' = 'Path' + 'System.Management.ManagementObjectSearcher' = { $($Input).Query.QueryString } + 'System.Net.IPAddress' = 'IPAddressToString' + 'System.Net.IPEndPoint' = { $($Input).Address.Address; $($Input).Port } + 'System.Net.Mail.MailAddress' = 'Address' + 'System.Net.NetworkInformation.PhysicalAddress' = '' + 'System.Security.Cryptography.X509Certificates.X500DistinguishedName' = 'Name' + 'System.Security.SecureString' = { ,[string[]]("(""$($Input | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)') } + 'System.Text.RegularExpressions.Regex' = '' + 'System.RuntimeType' = '' + 'System.Uri' = 'OriginalString' + 'System.Version' = '' + 'System.Void' = $Null } + hidden $StringBuilder + hidden [Int]$Offset = 0 + hidden [Int]$LineNumber = 1 - RemoveAt([Int]$Index) { - if ($Index -lt 0 -or $Index -ge $this.Value.Count) { Throw 'Index was out of range. Must be non-negative and less than the size of the collection.' } - if ($this.Value.GetType().GetMethod('RemoveAt')) { $null = $this._Value.removeAt($Index) } - else { - $this._Value = $(for ($i = 0; $i -lt $this._Value.Count; $i++) { - if ($i -ne $index) { $this._Value[$i] } - }) -as $this.ValueType - } - $this.Cache.Remove('ChildNodes') + PSSerialize($Object) { $this._Object = $Object } + PSSerialize($Object, $LanguageMode) { + $this._Object = $Object + $this.LanguageMode = $LanguageMode } - - [Object]GetChildNode([Int]$Index) { - if ($this.MaxDepthReached()) { return @() } - $Count = $this._Value.get_Count() - if ($Index -lt -$Count -or $Index -ge $Count) { throw "The $($this.Path) doesn't contain a child index: $Index" } - if ($Index -lt 0) { $Index = $Count + $Index } # Negative index - if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = [Dictionary[Int,Object]]::new() } - if ( - -not $this.Cache.ChildNode.ContainsKey($Index) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Index]._Value, $this._Value[$Index]) - ) { - $Node = [PSNode]::ParseInput($this._Value[$Index]) - $Node._Name = $Index - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Index] = $Node - } - return $this.Cache.ChildNode[$Index] + PSSerialize($Object, $LanguageMode, $ExpandDepth) { + $this._Object = $Object + $this.LanguageMode = $LanguageMode + $this.ExpandDepth = $ExpandDepth } - - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = for ($Index = 0; $Index -lt $this._Value.get_Count(); $Index++) { $this.GetChildNode($Index) } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } - } - return $this.Cache['ChildNodes'] + PSSerialize( + $Object, + $LanguageMode = 'Restricted', + $ExpandDepth = [Int]::MaxValue, + $Explicit = $False, + $FullTypeName = $False, + $HighFidelity = $False, + $ExpandSingleton = $False, + $Indent = ' ' + ) { + $this._Object = $Object + $this.LanguageMode = $LanguageMode + $this.ExpandDepth = $ExpandDepth + $this.Explicit = $Explicit + $this.FullTypeName = $FullTypeName + $this.HighFidelity = $HighFidelity + $this.ExpandSingleton = $ExpandSingleton + $this.Indent = $Indent } - [Int]GetHashCode($CaseSensitive) { - # The hash of a list node is equal if all items match the order and the case. - # The primary keys and the list type are not relevant - if ($null -eq $this._HashCode) { - $this._HashCode = [Dictionary[bool,int]]::new() - $this._ReferenceHashCode = [Dictionary[bool,int]]::new() - } - $ReferenceHashCode = $This._value.GetHashCode() - if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { - $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode - $HashCode = '@()'.GetHashCode() # Empty lists have a common hash that is not 0 - $Index = 0 - foreach ($Node in $this.GetNodeList()) { - $HashCode = $HashCode -bxor "$Index.$($Node.GetHashCode($CaseSensitive))".GetHashCode() - $index++ - } - $this._HashCode[$CaseSensitive] = $HashCode + hidden static [String[]]$Parameters = 'LanguageMode', 'Explicit', 'FullTypeName', 'HighFidelity', 'Indent', 'ExpandSingleton' + PSSerialize($Object, [HashTable]$Parameters) { + $this._Object = $Object + foreach ($Name in $Parameters.get_Keys()) { # https://github.com/PowerShell/PowerShell/issues/13307 + if ($Name -notin [PSSerialize]::Parameters) { Throw "Unknown parameter: $Name." } + $this.GetType().GetProperty($Name).SetValue($this, $Parameters[$Name]) } - return $this._HashCode[$CaseSensitive] - } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" } -} -Class PSMapNode : PSCollectionNode { - hidden static PSMapNode() { Use-ClassAccessors } - [Int]GetHashCode($CaseSensitive) { - # The hash of a map node is equal if all names and items match the order and the case. - # The map type is not relevant - if ($null -eq $this._HashCode) { - $this._HashCode = [Dictionary[bool,int]]::new() - $this._ReferenceHashCode = [Dictionary[bool,int]]::new() - } - $ReferenceHashCode = $This._value.GetHashCode() - if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { - $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode - $HashCode = '@{}'.GetHashCode() # Empty maps have a common hash that is not 0 - $Index = 0 - foreach ($Node in $this.GetNodeList()) { - $Name = if ($CaseSensitive) { $Node._Name } else { $Node._Name.ToUpper() } - $HashCode = $HashCode -bxor "$Index.$Name=$($Node.GetHashCode())".GetHashCode() - $Index++ - } - $this._HashCode[$CaseSensitive] = $HashCode + [String]Serialize($Object) { + if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } + if (-not ('ConstrainedLanguage', 'FullLanguage' -eq $this.LanguageMode)) { + if ($this.FullTypeName) { Write-Warning 'The FullTypeName switch requires Constrained - or FullLanguage mode.' } + if ($this.Explicit) { Write-Warning 'The Explicit switch requires Constrained - or FullLanguage mode.' } } - return $this._HashCode[$CaseSensitive] + if ($Object -is [PSNode]) { $Node = $Object } else { $Node = [PSNode]::ParseInput($Object) } + $this.StringBuilder = [System.Text.StringBuilder]::new() + $this.Stringify($Node) + return $this.StringBuilder.ToString() } -} -Class PSDictionaryNode : PSMapNode { - hidden static PSDictionaryNode() { Use-ClassAccessors } - hidden PSDictionaryNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } - } + hidden Stringify([PSNode]$Node) { + $Value = $Node.Value + $IsSubNode = $this.StringBuilder.Length -ne 0 + if ($Null -eq $Value) { + $this.StringBuilder.Append('$Null') + return + } + $Type = $Node.ValueType + $TypeName = "$Type" + $TypeInitializer = + if ($Null -ne $Type -and ( + $this.LanguageMode -eq 'Full' -or ( + $this.LanguageMode -eq 'Constrained' -and + [PSLanguageType]::IsConstrained($Type) -and ( + $this.Explicit -or -not ( + $Type.IsPrimitive -or + $Value -is [String] -or + $Value -is [Object[]] -or + $Value -is [Hashtable] + ) + ) + ) + ) + ) { + if ($this.FullTypeName) { + if ($Type.FullName -eq 'System.Management.Automation.PSCustomObject' ) { '[System.Management.Automation.PSObject]' } # https://github.com/PowerShell/PowerShell/issues/2295 + else { "[$($Type.FullName)]" } + } + elseif ($TypeName -eq 'System.Object[]') { "[Array]" } + elseif ($TypeName -eq 'System.Management.Automation.PSCustomObject') { "[PSCustomObject]" } + elseif ($Type.Name -eq 'RuntimeType') { "[Type]" } + else { "[$TypeName]" } + } + if ($TypeInitializer) { $this.StringBuilder.Append($TypeInitializer) } - hidden [Object]get_Count() { - return $this._Value.get_Count() - } + if ($Node -is [PSLeafNode] -or (-not $this.HighFidelity -and [PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName))) { + $MaxLength = if ($IsSubNode) { [PSSerialize]::MaxValueLength } else { [PSSerialize]::MaxLeafLength } - hidden [Object]get_Names() { - return ,$this._Value.get_Keys() - } + if ([PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName)) { + $Property = [PSSerialize]::RoundTripProperty[$Node.ValueType.FullName] + if ($Null -eq $Property) { $Expression = $Null } + elseif ($Property -is [String]) { $Expression = if ($Property) { ,$Value.$Property } else { "$Value" } } + elseif ($Property -is [ScriptBlock] ) { $Expression = Invoke-Command $Property -InputObject $Value } + elseif ($Property -is [HashTable]) { $Expression = if ($this.LanguageMode -eq 'Restricted') { $Null } else { @{} } } + elseif ($Property -is [Array]) { $Expression = @($Property.foreach{ $Value.$_ }) } + else { Throw "Unknown round trip property type: $($Property.GetType())."} + } + elseif ($Value -is [Type]) { $Expression = @() } + elseif ($Value -is [Attribute]) { $Expression = @() } + elseif ($Type.IsPrimitive) { $Expression = $Value } + elseif (-not $Type.GetConstructors()) { $Expression = "$TypeName" } + elseif ($Type.GetMethod('ToString', [Type[]]@())) { $Expression = $Value.ToString() } + elseif ($Value -is [Collections.ICollection]) { $Expression = ,$Value } + else { $Expression = $Value } # Handle compression - hidden [Object]get_Values() { - return ,$this._Value.get_Values() - } + if ($Null -eq $Expression) { $Expression = '$Null' } + elseif ($Expression -is [Bool]) { $Expression = "`$$Value" } + elseif ($Expression -is [Char]) { $Expression = "'$Value'" } + elseif ($Expression -is [ScriptBlock]) { $Expression = [Abbreviate]::new('{', $Expression, $MaxLength, '}') } + elseif ($Expression -is [HashTable]) { $Expression = '@{}' } + elseif ($Expression -is [Array]) { + if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [Abbreviate]::new('[', $Expression[0], $MaxLength, ']') } + else { + $Space = if ($this.ExpandDepth -ge 0) { ' ' } + $New = if ($TypeInitializer) { '::new(' } else { '@(' } + $Expression = $New + ($Expression.foreach{ + if ($Null -eq $_) { '$Null' } + elseif ($_.GetType().IsPrimitive) { "$_" } + elseif ($_ -is [Array]) { $_ -Join $Space } + else { "'$_'" } + } -Join ",$Space") + ')' + } + } + elseif ($Type -and $Type.IsPrimitive) { + if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [CommandColor]([String]$Expression[0]) } + } + else { + if ($Expression -isnot [String]) { $Expression = "$Expression" } + if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [StringColor]([Abbreviate]::new("'", $Expression, $MaxLength, "'")) } + else { + if ($Expression.Contains("`n")) { + $Expression = "@'" + [Environment]::NewLine + "$Expression".Replace("'", "''") + [Environment]::NewLine + "'@" + } + else { $Expression = "'$($Expression -Replace [PSSerialize]::AnySingleQuote, '$0$0')'" } + } + } - hidden [Object]get_CaseMatters() { #Returns Nullable[Boolean] - if (-not $this.Cache.ContainsKey('CaseMatters')) { - $this.Cache['CaseMatters'] = $null # else $Null means that there is no key with alphabetic characters in the dictionary - foreach ($Key in $this._Value.Get_Keys()) { - if ($Key -is [String] -and $Key -match '[a-z]') { - $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } - $this.Cache['CaseMatters'] = -not $this.Contains($Case) -or $Case -cin $this._Value.Get_Keys() - break + $this.StringBuilder.Append($Expression) + } + elseif ($Node -is [PSListNode]) { + $ChildNodes = $Node.get_ChildNodes() + $this.StringBuilder.Append('@(') + if ($this.LanguageMode -eq 'NoLanguage') { + if ($ChildNodes.Count -eq 0) { } + elseif ($IsSubNode) { $this.StringBuilder.Append([Abbreviate]::Ellipses) } + else { + $Indices = [PSSerialize]::NoLanguageIndices + if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } + $LastIndex = $Null + foreach ($Index in $Indices) { + if ($Null -ne $LastIndex) { $this.StringBuilder.Append(',') } + if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } + if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses),") } + $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) + $LastIndex = $Index + } + } + } + else { + $this.Offset++ + $StartLine = $this.LineNumber + $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -isnot [PSLeafNode]) + foreach ($ChildNode in $ChildNodes) { + if ($ChildNode.Name -gt 0) { + $this.StringBuilder.Append(',') + $this.NewWord() + } + else { + if ($ExpandSingle) { $this.NewWord('') } + if ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -is [PSListNode]) { $this.StringBuilder.Append(',') } + } + $this.Stringify($ChildNode) } + $this.Offset-- + if ($this.LineNumber -gt $StartLine) { $this.NewWord('') } } + $this.StringBuilder.Append(')') + } + else { # if ($Node -is [PSMapNode]) { + $ChildNodes = $Node.get_ChildNodes() + if ($ChildNodes) { + $this.StringBuilder.Append('@{') + if ($this.LanguageMode -eq 'NoLanguage') { + if ($ChildNodes.Count -gt 0) { + $Indices = [PSSerialize]::NoLanguageItems + if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } + $LastIndex = $Null + foreach ($Index in $Indices) { + if ($IsSubNode -and $Index) { $this.StringBuilder.Append(";$([Abbreviate]::Ellipses)"); break } + if ($Null -ne $LastIndex) { $this.StringBuilder.Append(';') } + if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } + if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses);") } + $this.StringBuilder.Append([VariableColor]( + [PSKeyExpression]::new($ChildNodes[$Index].Name, [PSSerialize]::MaxKeyLength))) + $this.StringBuilder.Append('=') + if ( + -not $IsSubNode -or + $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength -or + ($ChildNodes.Count -eq 1 -and $ChildNodes[$Index] -is [PSLeafNode]) + ) { + $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) + } + else { $this.StringBuilder.Append([Abbreviate]::Ellipses) } + $LastIndex = $Index + } + } + } + else { + $this.Offset++ + $StartLine = $this.LineNumber + $Index = 0 + $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or $ChildNodes[0] -isnot [PSLeafNode] + foreach ($ChildNode in $ChildNodes) { + if ($ChildNode.Name -eq 'TypeId' -and $Node._Value -is $ChildNode._Value) { continue } + if ($Index++) { + $Separator = if ($this.ExpandDepth -ge 0) { '; ' } else { ';' } + $this.NewWord($Separator) + } + elseif ($this.ExpandDepth -ge 0) { + if ($ExpandSingle) { $this.NewWord() } else { $this.StringBuilder.Append(' ') } + } + $this.StringBuilder.Append([PSKeyExpression]::new($ChildNode.Name, $this.LanguageMode, ($this.ExpandDepth -lt 0))) + if ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' = ') } else { $this.StringBuilder.Append('=') } + $this.Stringify($ChildNode) + } + $this.Offset-- + if ($this.LineNumber -gt $StartLine) { $this.NewWord() } + elseif ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' ') } + } + $this.StringBuilder.Append('}') + } + elseif ($Node -is [PSObjectNode] -and $TypeInitializer) { $this.StringBuilder.Append('::new()') } + else { $this.StringBuilder.Append('@{}') } } - return $this.Cache['CaseMatters'] } - [Bool]Contains($Key) { - if ($this._Value.GetType().GetMethod('ContainsKey')) { - return $this._Value.ContainsKey($Key) + hidden NewWord() { $this.NewWord(' ') } + hidden NewWord([String]$Separator) { + if ($this.Offset -le $this.ExpandDepth) { + $this.StringBuilder.AppendLine() + for($i = $this.Offset; $i -gt 0; $i--) { + $this.StringBuilder.Append($this.Indent) + } + $this.LineNumber++ } else { - return $this._Value.Contains($Key) + $this.StringBuilder.Append($Separator) } } - [Bool]Exists($Key) { return $this.Contains($Key) } - [Object]GetValue($Key) { return $this._Value[$Key] } - [Object]GetValue($Key, $Default) { - if (-not $This.Contains($Key)) { return $Default } - return $this._Value[$Key] + [String] ToString() { + if ($this._Object -is [PSNode]) { $Node = $this._Object } + else { $Node = [PSNode]::ParseInput($this._Object) } + $this.StringBuilder = [System.Text.StringBuilder]::new() + $this.Stringify($Node) + return $this.StringBuilder.ToString() } +} +Class ANSI { + # Retrieved from Get-PSReadLineOption + static [String]$CommandColor + static [String]$CommentColor + static [String]$ContinuationPromptColor + static [String]$DefaultTokenColor + static [String]$EmphasisColor + static [String]$ErrorColor + static [String]$KeywordColor + static [String]$MemberColor + static [String]$NumberColor + static [String]$OperatorColor + static [String]$ParameterColor + static [String]$SelectionColor + static [String]$StringColor + static [String]$TypeColor + static [String]$VariableColor - SetValue($Key, $Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value[$Key] = $Value - $this.Cache.Remove('ChildNodes') - } + # Hardcoded (if valid Get-PSReadLineOption) + static [String]$Reset + static [String]$ResetColor + static [String]$InverseColor + static [String]$InverseOff + + Static ANSI() { + # https://stackoverflow.com/questions/38045245/how-to-call-getstdhandle-getconsolemode-from-powershell + $MethodDefinitions = @' +[DllImport("kernel32.dll", SetLastError = true)] +public static extern IntPtr GetStdHandle(int nStdHandle); +[DllImport("kernel32.dll", SetLastError = true)] +public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); +'@ + $Kernel32 = Add-Type -MemberDefinition $MethodDefinitions -Name 'Kernel32' -Namespace 'Win32' -PassThru + $hConsoleHandle = $Kernel32::GetStdHandle(-11) # STD_OUTPUT_HANDLE + if (-not $Kernel32::GetConsoleMode($hConsoleHandle, [ref]0)) { return } - Add($Key, $Value) { - if ($this.Contains($Key)) { Throw "Item '$Key' has already been added." } - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value.Add($Key, $Value) - $this.Cache.Remove('ChildNodes') + $PSReadLineOption = try { Get-PSReadLineOption -ErrorAction SilentlyContinue } catch { $null } + if (-not $PSReadLineOption) { return } + $ANSIType = [ANSI] -as [Type] + foreach ($Property in [ANSI].GetProperties()) { + $PSReadLineProperty = $PSReadLineOption.PSObject.Properties[$Property.Name] + if ($PSReadLineProperty) { + $ANSIType.GetProperty($Property.Name).SetValue($Property.Name, $PSReadLineProperty.Value) + } + } + $Esc = [char]0x1b + [ANSI]::Reset = "$Esc[0m" + [ANSI]::ResetColor = "$Esc[39m" + [ANSI]::InverseColor = "$Esc[7m" + [ANSI]::InverseOff = "$Esc[27m" } - - Remove($Key) { - $null = $this._Value.Remove($Key) - $this.Cache.Remove('ChildNodes') +} +Class TextStyle { + hidden [String]$Text + hidden [String]$AnsiCode + hidden [String]$ResetCode = [ANSI]::Reset + TextStyle ([String]$Text, [String]$AnsiCode, [String]$ResetCode) { + $this.Text = $Text + $this.AnsiCode = $AnsiCode + $this.ResetCode = $ResetCode } - - hidden RemoveAt($Key) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) } - if (-not $this.Contains($Key)) { Throw "Item '$Key' doesn't exist." } - $null = $this._Value.Remove($Key) - $this.Cache.Remove('ChildNodes') + TextStyle ([String]$Text, [String]$AnsiCode) { + $this.Text = $Text + $this.AnsiCode = $AnsiCode } - - [Object]GetChildNode([Object]$Key) { - if ($this.MaxDepthReached()) { return @() } - if (-not $this.Contains($Key)) { Throw "The $($this.Path) doesn't contain a child named: $Key" } - if (-not $this.Cache.ContainsKey('ChildNode')) { - # The ChildNode cache case sensitivity is based on the current dictionary population. - # The ChildNode cache is always ordinal, if the contained dictionary is invariant, extra entries might - # appear in the cache but shouldn't effect the results other than slightly slow down the performance. - # In other words, do not use the cache to count the entries. Custom comparers are not supported. - $this.Cache['ChildNode'] = if ($this.get_CaseMatters()) { [HashTable]::new() } else { @{} } # default is case insensitive - } - elseif ( - -not $this.Cache.ChildNode.ContainsKey($Key) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key]._Value, $this._Value[$Key]) - ) { - if($null -eq $this.get_CaseMatters()) { # If the case was undetermined, check the new key for case sensitivity - $this.Cache.CaseMatters = if ($Key -is [String] -and $Key -match '[a-z]') { - $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } - -not $this._Value.Contains($Case) -or $Case -cin $this._Value.Get_Keys() - } - if ($this.get_CaseMatters()) { - $ChildNode = $this.Cache['ChildNode'] - $this.Cache['ChildNode'] = [HashTable]::new() # Create a new cache as it appears to be case sensitive - foreach ($Key in $ChildNode.get_Keys()) { # Migrate the content - $this.Cache.ChildNode[$Key] = $ChildNode[$Key] - } - } - } + [String] ToString() { + if ($this.ResetCode -eq [ANSI]::ResetColor) { + return "$($this.AnsiCode)$($this.Text.Replace($this.ResetCode, $this.AnsiCode))$($this.ResetCode)" } - if ( - -not $this.Cache.ChildNode.ContainsKey($Key) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key].Value, $this._Value[$Key]) - ) { - $Node = [PSNode]::ParseInput($this._Value[$Key]) - $Node._Name = $Key - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Key] = $Node + else { + return "$($this.AnsiCode)$($this.Text)$($this.ResetCode)" } - return $this.Cache.ChildNode[$Key] } +} +Class TextColor : TextStyle { TextColor($Text, $AnsiColor) : base($Text, $AnsiColor, [ANSI]::ResetColor) {} } +Class CommandColor : TextColor { CommandColor($Text) : base($Text, [ANSI]::CommandColor) {} } +Class CommentColor : TextColor { CommentColor($Text) : base($Text, [ANSI]::CommentColor) {} } +Class ContinuationPromptColor : TextColor { ContinuationPromptColor($Text) : base($Text, [ANSI]::ContinuationPromptColor) {} } +Class DefaultTokenColor : TextColor { DefaultTokenColor($Text) : base($Text, [ANSI]::DefaultTokenColor) {} } +Class EmphasisColor : TextColor { EmphasisColor($Text) : base($Text, [ANSI]::EmphasisColor) {} } +Class ErrorColor : TextColor { ErrorColor($Text) : base($Text, [ANSI]::ErrorColor) {} } +Class KeywordColor : TextColor { KeywordColor($Text) : base($Text, [ANSI]::KeywordColor) {} } +Class MemberColor : TextColor { MemberColor($Text) : base($Text, [ANSI]::MemberColor) {} } +Class NumberColor : TextColor { NumberColor($Text) : base($Text, [ANSI]::NumberColor) {} } +Class OperatorColor : TextColor { OperatorColor($Text) : base($Text, [ANSI]::OperatorColor) {} } +Class ParameterColor : TextColor { ParameterColor($Text) : base($Text, [ANSI]::ParameterColor) {} } +Class SelectionColor : TextColor { SelectionColor($Text) : base($Text, [ANSI]::SelectionColor) {} } +Class StringColor : TextColor { StringColor($Text) : base($Text, [ANSI]::StringColor) {} } +Class TypeColor : TextColor { TypeColor($Text) : base($Text, [ANSI]::TypeColor) {} } +Class VariableColor : TextColor { VariableColor($Text) : base($Text, [ANSI]::VariableColor) {} } +Class InverseColor : TextStyle { InverseColor($Text) : base($Text, [ANSI]::InverseColor, [ANSI]::InverseOff) {} } +class XdnName { + hidden [Bool]$_Literal + hidden $_IsVerbatim + hidden $_ContainsWildcard + hidden $_Value - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = foreach ($Key in $this._Value.get_Keys()) { $this.GetChildNode($Key) } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } + hidden Initialize($Value, $Literal) { + $this._Value = $Value + if ($Null -ne $Literal) { $this._Literal = $Literal } else { $this._Literal = $this.IsVerbatim() } + if ($this._Literal) { + $XdnName = [XdnName]::new() + $XdnName._ContainsWildcard = $False + } + else { + $XdnName = [XdnName]::new() + $XdnName._ContainsWildcard = $null } - return $this.Cache['ChildNodes'] - } - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" } -} -Class PSObjectNode : PSMapNode { - hidden static PSObjectNode() { Use-ClassAccessors } + XdnName() {} + XdnName($Value) { $this.Initialize($Value, $null) } + XdnName($Value, [Bool]$Literal) { $this.Initialize($Value, $Literal) } + static [XdnName]Literal($Value) { return [XdnName]::new($Value, $true) } + static [XdnName]Expression($Value) { return [XdnName]::new($Value, $false) } - hidden PSObjectNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } + [Bool] IsVerbatim() { + if ($Null -eq $this._IsVerbatim) { + $this._IsVerbatim = $this._Value -is [String] -and $this._Value -Match '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices + } + return $this._IsVerbatim } - hidden [Object]get_Count() { - return @($this._Value.PSObject.Properties).get_Count() + [Bool] ContainsWildcard() { + if ($Null -eq $this._ContainsWildcard) { + $this._ContainsWildcard = $this._Value -is [String] -and $this._Value -Match '(?<=([^`]|^)(``)*)[\?\*]' + } + return $this._ContainsWildcard } - hidden [Object]get_Names() { - return ,$this._Value.PSObject.Properties.Name + [Bool] Equals($Object) { + if ($this._Literal) { return $this._Value -eq $Object } + elseif ($this.ContainsWildcard()) { return $Object -Like $this._Value } + else { return $this._Value -eq $Object } } - hidden [Object]get_Values() { - return ,$this._Value.PSObject.Properties.Value + [String] ToString($Colored) { + $Color = if ($Colored) { + if ($this._Literal) { [ANSI]::VariableColor } + elseif (-not $this.IsVerbatim()) { [ANSI]::StringColor } + elseif ($this.ContainsWildcard()) { [ANSI]::EmphasisColor } + else { [ANSI]::VariableColor } + } + $String = + if ($this._Literal) { "'" + "$($this._Value)".Replace("'", "''") + "'" } + else { "$($this._Value)" -replace '(?')) doesn't contain a child named: $Name" } - if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = @{} } # Object properties are case insensitive - if ( - -not $this.Cache.ChildNode.ContainsKey($Name) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Name]._Value, $this._Value.PSObject.Properties[$Name].Value) - ) { - $Node = [PSNode]::ParseInput($this._Value.PSObject.Properties[$Name].Value) - $Node._Name = $Name - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Name] = $Node - } - return $this.Cache.ChildNode[$Name] - } + [String] ToString([String]$VariableName, [Bool]$Colored) { + $RegularColor = if ($Colored) { [ANSI]::VariableColor } + $OperatorColor = if ($Colored) { [ANSI]::CommandColor } + $ErrorColor = if ($Colored) { [ANSI]::ErrorColor } + $ResetColor = if ($Colored) { [ANSI]::ResetColor } - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = foreach ($Property in $this._Value.PSObject.Properties) { $this.GetChildNode($Property.Name) } - # if ($Property.Value -isnot [Reflection.MemberInfo]) { $this.GetChildNode($Property.Name) } - # } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } + $Path = [System.Text.StringBuilder]::new() + $PreviousEntry = $Null + foreach ($Entry in $this._Entries) { + $Value = $Entry.Value + $Append = Switch ($Entry.Key) { + Root { "$OperatorColor$VariableName" } + Ancestor { "$OperatorColor$('.' * $Value)" } + Index { + $Dot = if (-not $PreviousEntry -or $PreviousEntry.Key -eq 'Ancestor') { "$OperatorColor." } + if ([int]::TryParse($Value, [Ref]$Null)) { "$Dot$RegularColor[$Value]" } + else { "$ErrorColor[$Value]" } + } + Child { "$RegularColor.$(@($Value).foreach{ $_.ToString($Colored) } -Join ""$OperatorColor/"")" } + Descendant { "$OperatorColor~$(@($Value).foreach{ $_.ToString($Colored) } -Join ""$OperatorColor/"")" } + Offspring { "$OperatorColor~~$(@($Value).foreach{ $_.ToString($Colored) } -Join ""$OperatorColor/"")" } + Equals { "$OperatorColor=$(@($Value).foreach{ $_.ToString($Colored) } -Join ""$OperatorColor/"")" } + Default { "$ErrorColor$($Value)" } + } + $Path.Append($Append) + $PreviousEntry = $Entry } - return $this.Cache['ChildNodes'] + $Path.Append($ResetColor) + return $Path.ToString() } + [String] ToString() { return $this.ToString($Null , $False)} + [String] ToString([String]$VariableName) { return $this.ToString($VariableName, $False)} + [String] ToColoredString() { return $this.ToString($Null, $True)} + [String] ToColoredString([String]$VariableName) { return $this.ToString($VariableName, $True)} - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" + static XdnPath() { + Use-ClassAccessors } } -class PSListNodeComparer : ObjectComparer, IComparer[Object] { # https://github.com/PowerShell/PowerShell/issues/23959 - PSListNodeComparer () {} - PSListNodeComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - PSListNodeComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - PSListNodeComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - PSListNodeComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - [int] Compare ([Object]$Node1, [Object]$Node2) { return $this.CompareRecurse($Node1, $Node2, 'Compare') } -} -Class TextColor : TextStyle { TextColor($Text, $AnsiColor) : base($Text, $AnsiColor, [ANSI]::ResetColor) {} } -Class CommandColor : TextColor { CommandColor($Text) : base($Text, [ANSI]::CommandColor) {} } -Class CommentColor : TextColor { CommentColor($Text) : base($Text, [ANSI]::CommentColor) {} } -Class ContinuationPromptColor : TextColor { ContinuationPromptColor($Text) : base($Text, [ANSI]::ContinuationPromptColor) {} } -Class DefaultTokenColor : TextColor { DefaultTokenColor($Text) : base($Text, [ANSI]::DefaultTokenColor) {} } -Class EmphasisColor : TextColor { EmphasisColor($Text) : base($Text, [ANSI]::EmphasisColor) {} } -Class ErrorColor : TextColor { ErrorColor($Text) : base($Text, [ANSI]::ErrorColor) {} } -Class KeywordColor : TextColor { KeywordColor($Text) : base($Text, [ANSI]::KeywordColor) {} } -Class MemberColor : TextColor { MemberColor($Text) : base($Text, [ANSI]::MemberColor) {} } -Class NumberColor : TextColor { NumberColor($Text) : base($Text, [ANSI]::NumberColor) {} } -Class OperatorColor : TextColor { OperatorColor($Text) : base($Text, [ANSI]::OperatorColor) {} } -Class ParameterColor : TextColor { ParameterColor($Text) : base($Text, [ANSI]::ParameterColor) {} } -Class SelectionColor : TextColor { SelectionColor($Text) : base($Text, [ANSI]::SelectionColor) {} } -Class StringColor : TextColor { StringColor($Text) : base($Text, [ANSI]::StringColor) {} } -Class TypeColor : TextColor { TypeColor($Text) : base($Text, [ANSI]::TypeColor) {} } -Class VariableColor : TextColor { VariableColor($Text) : base($Text, [ANSI]::VariableColor) {} } -Class InverseColor : TextStyle { InverseColor($Text) : base($Text, [ANSI]::InverseColor, [ANSI]::InverseOff) {} } #EndRegion Class @@ -2390,66 +2529,66 @@ if (@(`$Invoke).Count -gt 1) { `$Output } else { ,`$Output } function Compare-ObjectGraph { <# .SYNOPSIS - Compare Object Graph +Compare Object Graph .DESCRIPTION - Deep compares two Object Graph and lists the differences between them. +Deep compares two Object Graph and lists the differences between them. .PARAMETER InputObject - The input object that will be compared with the reference object (see: [-Reference] parameter). +The input object that will be compared with the reference object (see: [-Reference] parameter). - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: +> [!NOTE] +> Multiple input object might be provided via the pipeline. +> The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. +> To avoid a list of (root) objects to unroll, use the **comma operator**: - ,$InputObject | Compare-ObjectGraph $Reference. + ,$InputObject | Compare-ObjectGraph $Reference. .PARAMETER Reference - The reference that is used to compared with the input object (see: [-InputObject] parameter). +The reference that is used to compared with the input object (see: [-InputObject] parameter). .PARAMETER PrimaryKey - If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched - based on the values of the `-PrimaryKey` supplied. +If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched +based on the values of the `-PrimaryKey` supplied. .PARAMETER IsEqual - If set, the cmdlet will return a boolean (`$true` or `$false`). - As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. +If set, the cmdlet will return a boolean (`$true` or `$false`). +As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. .PARAMETER MatchCase - Unless the `-MatchCase` switch is provided, string values are considered case insensitive. +Unless the `-MatchCase` switch is provided, string values are considered case insensitive. - > [!NOTE] - > Dictionary keys are compared based on the `$Reference`. - > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison - > is case insensitive otherwise the comparer supplied with the dictionary is used. +> [!NOTE] +> Dictionary keys are compared based on the `$Reference`. +> if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison +> is case insensitive otherwise the comparer supplied with the dictionary is used. .PARAMETER MatchType - Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the - `$Reference` object is leading. Meaning `$Reference -eq $InputObject`: +Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the +`$Reference` object is leading. Meaning `$Reference -eq $InputObject`: - '1.0' -eq 1.0 # $false - 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) + '1.0' -eq 1.0 # $false + 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) .PARAMETER IgnoreLisOrder - By default, items in a list are matched independent of the order (meaning by index position). - If the `-IgnoreListOrder` switch is supplied, any list in the `$InputObject` is searched for a match - with the reference. +By default, items in a list are matched independent of the order (meaning by index position). +If the `-IgnoreListOrder` switch is supplied, any list in the `$InputObject` is searched for a match +with the reference. - > [!NOTE] - > Regardless the list order, any dictionary lists are matched by the primary key (if supplied) first. +> [!NOTE] +> Regardless the list order, any dictionary lists are matched by the primary key (if supplied) first. .PARAMETER MatchMapOrder - By default, items in dictionary (including properties of an PSCustomObject or Component Object) are - matched by their key name (independent of the order). - If the `-MatchMapOrder` switch is supplied, each entry is also validated by the position. +By default, items in dictionary (including properties of an PSCustomObject or Component Object) are +matched by their key name (independent of the order). +If the `-MatchMapOrder` switch is supplied, each entry is also validated by the position. - > [!NOTE] - > A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, - the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. +> [!NOTE] +> A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, +the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. .PARAMETER MaxDepth - The maximal depth to recursively compare each embedded property (default: 10). +The maximal depth to recursively compare each embedded property (default: 10). #> [CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Compare-ObjectGraph.md')] param( @@ -2496,43 +2635,43 @@ process { function ConvertFrom-Expression { <# .SYNOPSIS - Deserializes a PowerShell expression to an object. +Deserializes a PowerShell expression to an object. .DESCRIPTION - The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph - existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. +The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph +existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. .PARAMETER InputObject - Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, - or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. +Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, +or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. - The **InputObject** parameter is required, but its value can be an empty string. - The **InputObject** value can't be `$null` or an empty string. +The **InputObject** parameter is required, but its value can be an empty string. +The **InputObject** value can't be `$null` or an empty string. .PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] - - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. +Defines which object types are allowed for the deserialization, see: [About language modes][2] + +* Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + `[String]`, `[Array]` or `[HashTable]`. +* Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. + +> [!Caution] +> +> In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, +> CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. +> +> Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. +> Verify that the class types in the expression are safe before instantiating them. In general, it is +> best to design your configuration expressions with restricted or constrained classes, rather than +> allowing full freeform expressions. .PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given list type. +If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown +or denied type initializer will be converted to the given list type. .PARAMETER MapAs - If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given map (dictionary or object) type. +If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown +or denied type initializer will be converted to the given map (dictionary or object) type. #> @@ -2590,112 +2729,112 @@ process { function ConvertTo-Expression { <# .SYNOPSIS - Serializes an object to a PowerShell expression. +Serializes an object to a PowerShell expression. .DESCRIPTION - The ConvertTo-Expression cmdlet converts (serializes) an object to a PowerShell expression. - The object can be stored in a variable, (.psd1) file or any other common storage for later use or to be ported - to another system. +The ConvertTo-Expression cmdlet converts (serializes) an object to a PowerShell expression. +The object can be stored in a variable, (.psd1) file or any other common storage for later use or to be ported +to another system. - expressions might be restored to an object using the native [Invoke-Expression] cmdlet: +expressions might be restored to an object using the native [Invoke-Expression] cmdlet: - $Object = Invoke-Expression ($Object | ConvertTo-Expression) + $Object = Invoke-Expression ($Object | ConvertTo-Expression) - > [!Warning] - > Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using `Invoke-Expression` - > to run a command that the user enters, verify that the command is safe to run before running it. - > In general, it is best to restore your objects using [ConvertFrom-Expression]. +> [!Warning] +> Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using `Invoke-Expression` +> to run a command that the user enters, verify that the command is safe to run before running it. +> In general, it is best to restore your objects using [ConvertFrom-Expression]. - > [!Note] - > Some object types can not be reconstructed from a simple serialized expression. +> [!Note] +> Some object types can not be reconstructed from a simple serialized expression. .INPUTS - Any. Each objects provided through the pipeline will converted to an expression. To concatenate all piped - objects in a single expression, use the unary comma operator, e.g.: `,$Object | ConvertTo-Expression` +Any. Each objects provided through the pipeline will converted to an expression. To concatenate all piped +objects in a single expression, use the unary comma operator, e.g.: `,$Object | ConvertTo-Expression` .OUTPUTS - String[]. `ConvertTo-Expression` returns a PowerShell [String] expression for each input object. +String[]. `ConvertTo-Expression` returns a PowerShell [String] expression for each input object. .PARAMETER InputObject - Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, - or type a command or expression that gets the objects. You can also pipe one or more objects to - `ConvertTo-Expression.` +Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, +or type a command or expression that gets the objects. You can also pipe one or more objects to +`ConvertTo-Expression.` .PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: +Defines which object types are allowed for the serialization, see: [About language modes][2] +If a specific type isn't allowed in the given language mode, it will be substituted by: - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) +* **`$Null`** in case of a null value +* **`$False`** in case of a boolean false +* **`$True`** in case of a boolean true +* **A number** in case of a primitive value +* **A string** in case of a string or any other **leaf** node +* `@(...)` for an array (**list** node) +* `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - See the [PSNode Object Parser][1] for a detailed definition on node types. +See the [PSNode Object Parser][1] for a detailed definition on node types. .PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. +Defines up till what level the collections will be expanded in the output. - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. +* A `-ExpandDepth 0` will create a single line expression. +* A `-ExpandDepth -1` will compress the single line by removing command spaces. - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. +> [!Note] +> White spaces (as newline characters and spaces) will not be removed from the content +> of a (here) string. .PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) +By default, restricted language types initializers are suppressed. +When the `Explicit` switch is set, *all* values will be prefixed with an initializer +(as e.g. `[Long]` and `[Array]`) - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode +> [!Note] +> The `-Explicit` switch can not be used in **restricted** language mode .PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. +In case a value is prefixed with an initializer, the full type name of the initializer is used. - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). +> [!Note] +> The `-FullTypename` switch can not be used in **restricted** language mode and will only be +> meaningful if the initializer is used (see also the [-Explicit] switch). .PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. +If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - By default the fidelity of an object expression will end if: +By default the fidelity of an object expression will end if: - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. +1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) +2) the (embedded) object expression is able to round trip. - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. +An object is able to roundtrip if the resulted expression of the object itself or one of +its properties (prefixed with the type initializer) can be used to rebuild the object. - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). +The advantage of the default fidelity is that the resulted expression round trips (aka the +object might be rebuild from the expression), the disadvantage is that information hold by +less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. +The advantage of the high fidelity switch is that all the information of the underlying +properties is shown, yet any constrained or full object type will likely fail to rebuild +due to constructor limitations such as readonly property. - > [!Note] - > The Object property `TypeId = []` is always excluded. +> [!Note] +> The Object property `TypeId = []` is always excluded. .PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. +(List or map) collections nodes that contain a single item will not be expanded unless this +`-ExpandSingleton` is supplied. .PARAMETER IndentSize - Specifies indent used for the nested properties. +Specifies indent used for the nested properties. .PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). +Specifies how many levels of contained objects are included in the PowerShell representation. +The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('cto')] @@ -2755,45 +2894,45 @@ process { function Copy-ObjectGraph { <# .SYNOPSIS - Copy object graph +Copy object graph .DESCRIPTION - Recursively ("deep") copies a object graph. +Recursively ("deep") copies a object graph. .EXAMPLE - # Deep copy a complete object graph into a new object graph +# Deep copy a complete object graph into a new object graph - $NewObjectGraph = Copy-ObjectGraph $ObjectGraph + $NewObjectGraph = Copy-ObjectGraph $ObjectGraph .EXAMPLE - # Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects +# Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects - $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject + $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject .EXAMPLE - # Convert a Json string to an object graph with (case insensitive) ordered dictionaries +# Convert a Json string to an object graph with (case insensitive) ordered dictionaries - $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered]@{}) + $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered]@{}) .PARAMETER InputObject - The input object that will be recursively copied. +The input object that will be recursively copied. .PARAMETER ListAs - If supplied, lists will be converted to the given type (or type of the supplied object example). +If supplied, lists will be converted to the given type (or type of the supplied object example). .PARAMETER DictionaryAs - If supplied, dictionaries will be converted to the given type (or type of the supplied object example). - This parameter also accepts the [`PSCustomObject`][1] types - By default (if the [-DictionaryAs] parameters is omitted), - [`Component`][2] objects will be converted to a [`PSCustomObject`][1] type. +If supplied, dictionaries will be converted to the given type (or type of the supplied object example). +This parameter also accepts the [`PSCustomObject`][1] types +By default (if the [-DictionaryAs] parameters is omitted), +[`Component`][2] objects will be converted to a [`PSCustomObject`][1] type. .PARAMETER ExcludeLeafs - If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. - If omitted, each leaf will be shallow copied +If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. +If omitted, each leaf will be shallow copied .LINK - [1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" - [2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" +[1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" +[2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" #> [Alias('Copy-Object', 'cpo')] [OutputType([Object[]])] @@ -2874,100 +3013,100 @@ process { function Export-ObjectGraph { <# .SYNOPSIS - Serializes a PowerShell File or object-graph and exports it to a PowerShell (data) file. +Serializes a PowerShell File or object-graph and exports it to a PowerShell (data) file. .DESCRIPTION - The `Export-ObjectGraph` cmdlet converts a PowerShell (complex) object to an PowerShell expression - and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file. +The `Export-ObjectGraph` cmdlet converts a PowerShell (complex) object to an PowerShell expression +and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file. .PARAMETER Path - Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. - Wildcard characters are permitted. +Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. +Wildcard characters are permitted. .PARAMETER LiteralPath - Specifies a path to one or more locations where PowerShell should export the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. +Specifies a path to one or more locations where PowerShell should export the object-graph. +The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. +If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell +PowerShell not to interpret any characters as escape sequences. .PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: +Defines which object types are allowed for the serialization, see: [About language modes][2] +If a specific type isn't allowed in the given language mode, it will be substituted by: - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) +* **`$Null`** in case of a null value +* **`$False`** in case of a boolean false +* **`$True`** in case of a boolean true +* **A number** in case of a primitive value +* **A string** in case of a string or any other **leaf** node +* `@(...)` for an array (**list** node) +* `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - See the [PSNode Object Parser][1] for a detailed definition on node types. +See the [PSNode Object Parser][1] for a detailed definition on node types. .PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. +Defines up till what level the collections will be expanded in the output. - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. +* A `-ExpandDepth 0` will create a single line expression. +* A `-ExpandDepth -1` will compress the single line by removing command spaces. - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. +> [!Note] +> White spaces (as newline characters and spaces) will not be removed from the content +> of a (here) string. .PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) +By default, restricted language types initializers are suppressed. +When the `Explicit` switch is set, *all* values will be prefixed with an initializer +(as e.g. `[Long]` and `[Array]`) - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode +> [!Note] +> The `-Explicit` switch can not be used in **restricted** language mode .PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. +In case a value is prefixed with an initializer, the full type name of the initializer is used. - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). +> [!Note] +> The `-FullTypename` switch can not be used in **restricted** language mode and will only be +> meaningful if the initializer is used (see also the [-Explicit] switch). .PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. +If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - By default the fidelity of an object expression will end if: +By default the fidelity of an object expression will end if: - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. +1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) +2) the (embedded) object expression is able to round trip. - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. +An object is able to roundtrip if the resulted expression of the object itself or one of +its properties (prefixed with the type initializer) can be used to rebuild the object. - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). +The advantage of the default fidelity is that the resulted expression round trips (aka the +object might be rebuild from the expression), the disadvantage is that information hold by +less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. +The advantage of the high fidelity switch is that all the information of the underlying +properties is shown, yet any constrained or full object type will likely fail to rebuild +due to constructor limitations such as readonly property. - > [!Note] - > Objects properties of type `[Reflection.MemberInfo]` are always excluded. +> [!Note] +> Objects properties of type `[Reflection.MemberInfo]` are always excluded. .PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. +(List or map) collections nodes that contain a single item will not be expanded unless this +`-ExpandSingleton` is supplied. .PARAMETER IndentSize - Specifies indent used for the nested properties. +Specifies indent used for the nested properties. .PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). +Specifies how many levels of contained objects are included in the PowerShell representation. +The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). .PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. +Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('Export-Object', 'epo')] @@ -3034,161 +3173,161 @@ end { function Get-ChildNode { <# .SYNOPSIS - Gets the child nodes of an object-graph +Gets the child nodes of an object-graph .DESCRIPTION - Gets the (unique) nodes and child nodes in one or more specified locations of an object-graph - The returned nodes are unique even if the provide list of input parent nodes have an overlap. +Gets the (unique) nodes and child nodes in one or more specified locations of an object-graph +The returned nodes are unique even if the provide list of input parent nodes have an overlap. .EXAMPLE - # Select all leaf nodes in a object graph +# Select all leaf nodes in a object graph - Given the following object graph: +Given the following object graph: - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } + $Object = @{ + Comment = 'Sample ObjectGraph' + Data = @( + @{ + Index = 1 + Name = 'One' + Comment = 'First item' + } + @{ + Index = 2 + Name = 'Two' + Comment = 'Second item' + } + @{ + Index = 3 + Name = 'Three' + Comment = 'Third item' + } + ) + } - The following example will receive all leaf nodes: +The following example will receive all leaf nodes: - $Object | Get-ChildNode -Recurse -Leaf + $Object | Get-ChildNode -Recurse -Leaf - Path Name Depth Value - ---- ---- ----- ----- - .Data[0].Comment Comment 3 First item - .Data[0].Name Name 3 One - .Data[0].Index Index 3 1 - .Data[1].Comment Comment 3 Second item - .Data[1].Name Name 3 Two - .Data[1].Index Index 3 2 - .Data[2].Comment Comment 3 Third item - .Data[2].Name Name 3 Three - .Data[2].Index Index 3 3 - .Comment Comment 1 Sample ObjectGraph + Path Name Depth Value + ---- ---- ----- ----- + .Data[0].Comment Comment 3 First item + .Data[0].Name Name 3 One + .Data[0].Index Index 3 1 + .Data[1].Comment Comment 3 Second item + .Data[1].Name Name 3 Two + .Data[1].Index Index 3 2 + .Data[2].Comment Comment 3 Third item + .Data[2].Name Name 3 Three + .Data[2].Index Index 3 3 + .Comment Comment 1 Sample ObjectGraph .EXAMPLE - # update a property +# update a property - The following example selects all child nodes named `Comment` at a depth of `3`. - Than filters the one that has an `Index` sibling with the value `2` and eventually - sets the value (of the `Comment` node) to: 'Two to the Loo'. +The following example selects all child nodes named `Comment` at a depth of `3`. +Than filters the one that has an `Index` sibling with the value `2` and eventually +sets the value (of the `Comment` node) to: 'Two to the Loo'. - $Object | Get-ChildNode -AtDepth 3 -Include Comment | - Where-Object { $_.ParentNode.GetChildNode('Index').Value -eq 2 } | - ForEach-Object { $_.Value = 'Two to the Loo' } + $Object | Get-ChildNode -AtDepth 3 -Include Comment | + Where-Object { $_.ParentNode.GetChildNode('Index').Value -eq 2 } | + ForEach-Object { $_.Value = 'Two to the Loo' } - ConvertTo-Expression $Object + ConvertTo-Expression $Object - @{ - Data = - @{ - Comment = 'First item' - Name = 'One' - Index = 1 - }, - @{ - Comment = 'Two to the Loo' - Name = 'Two' - Index = 2 - }, - @{ - Comment = 'Third item' - Name = 'Three' - Index = 3 - } - Comment = 'Sample ObjectGraph' - } + @{ + Data = + @{ + Comment = 'First item' + Name = 'One' + Index = 1 + }, + @{ + Comment = 'Two to the Loo' + Name = 'Two' + Index = 2 + }, + @{ + Comment = 'Third item' + Name = 'Three' + Index = 3 + } + Comment = 'Sample ObjectGraph' + } - See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties and methods. +See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties and methods. .PARAMETER InputObject - The concerned object graph or node. +The concerned object graph or node. .PARAMETER Recurse - Recursively iterates through all embedded property objects (nodes) to get the selected nodes. - The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` - of the (root) node. To change the maximum depth the (root) node needs to be loaded first, e.g.: +Recursively iterates through all embedded property objects (nodes) to get the selected nodes. +The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` +of the (root) node. To change the maximum depth the (root) node needs to be loaded first, e.g.: - Get-Node -Depth 20 | Get-ChildNode ... + Get-Node -Depth 20 | Get-ChildNode ... - (See also: [`Get-Node`][2]) +(See also: [`Get-Node`][2]) - > [!NOTE] - > If the [AtDepth] parameter is supplied, the object graph is recursively searched anyways - > for the selected nodes up till the deepest given `AtDepth` value. +> [!NOTE] +> If the [AtDepth] parameter is supplied, the object graph is recursively searched anyways +> for the selected nodes up till the deepest given `AtDepth` value. .PARAMETER AtDepth - When defined, only returns nodes at the given depth(s). +When defined, only returns nodes at the given depth(s). - > [!NOTE] - > The nodes below the `MaxDepth` can not be retrieved. +> [!NOTE] +> The nodes below the `MaxDepth` can not be retrieved. .PARAMETER ListChild - Returns the closest nodes derived from a **list node**. +Returns the closest nodes derived from a **list node**. .PARAMETER Include - Returns only nodes derived from a **map node** including only the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. +Returns only nodes derived from a **map node** including only the ones specified by one or more +string patterns defined by this parameter. Wildcard characters are permitted. - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. +> [!NOTE] +> The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied +> after the inclusions, which can affect the final output. .PARAMETER Exclude - Returns only nodes derived from a **map node** excluding the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. +Returns only nodes derived from a **map node** excluding the ones specified by one or more +string patterns defined by this parameter. Wildcard characters are permitted. - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. +> [!NOTE] +> The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied +> after the inclusions, which can affect the final output. .PARAMETER Literal - The values of the [-Include] - and [-Exclude] parameters are used exactly as it is typed. - No characters are interpreted as wildcards. +The values of the [-Include] - and [-Exclude] parameters are used exactly as it is typed. +No characters are interpreted as wildcards. .PARAMETER Leaf - Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. - You can use the [-Recurse] parameter with the [-Leaf] parameter. +Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. +You can use the [-Recurse] parameter with the [-Leaf] parameter. .PARAMETER IncludeSelf - Includes the current node with the returned child nodes. +Includes the current node with the returned child nodes. .PARAMETER ValueOnly - returns the value of the node instead of the node itself. +returns the value of the node instead of the node itself. .PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: +Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. +The failsafe will prevent infinitive loops for circular references as e.g. in: - $Test = @{Guid = New-Guid} - $Test.Parent = $Test + $Test = @{Guid = New-Guid} + $Test.Parent = $Test - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. +The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. +> [!Note] +> The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node +> at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" #> [Alias('gcn')] @@ -3278,113 +3417,113 @@ process { function Get-Node { <# .SYNOPSIS - Get a node +Get a node .DESCRIPTION - The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. +The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. .EXAMPLE - # Parse a object graph to a node instance +# Parse a object graph to a node instance - The following example parses a hash table to `[PSNode]` instance: +The following example parses a hash table to `[PSNode]` instance: - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node + @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node - PathName Name Depth Value - -------- ---- ----- ----- - 0 {My, Object} + PathName Name Depth Value + -------- ---- ----- ----- + 0 {My, Object} .EXAMPLE - # select a sub node in an object graph +# select a sub node in an object graph - The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) - item in the `My` map node +The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) +item in the `My` map node - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node My[1] + @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node My[1] - PathName Name Depth Value - -------- ---- ----- ----- - My[1] 1 2 2 + PathName Name Depth Value + -------- ---- ----- ----- + My[1] 1 2 2 .EXAMPLE - # Change the price of the **PowerShell** book: - - $ObjectGraph = - @{ - BookStore = @( - @{ - Book = @{ - Title = 'Harry Potter' - Price = 29.99 - } - }, - @{ - Book = @{ - Title = 'Learning PowerShell' - Price = 39.95 - } - } - ) - } +# Change the price of the **PowerShell** book: - ($ObjectGraph | Get-Node BookStore~Title=*PowerShell*..Price).Value = 24.95 - $ObjectGraph | ConvertTo-Expression + $ObjectGraph = @{ BookStore = @( @{ Book = @{ - Price = 29.99 Title = 'Harry Potter' + Price = 29.99 } }, @{ Book = @{ - Price = 24.95 Title = 'Learning PowerShell' + Price = 39.95 } } ) } - for more details, see: [PowerShell Object Parser][1] and [Extended dot notation][2] + ($ObjectGraph | Get-Node BookStore~Title=*PowerShell*..Price).Value = 24.95 + $ObjectGraph | ConvertTo-Expression + @{ + BookStore = @( + @{ + Book = @{ + Price = 29.99 + Title = 'Harry Potter' + } + }, + @{ + Book = @{ + Price = 24.95 + Title = 'Learning PowerShell' + } + } + ) + } + +for more details, see: [PowerShell Object Parser][1] and [Extended dot notation][2] .PARAMETER InputObject - The concerned object graph or node. +The concerned object graph or node. .PARAMETER Path - Specifies the path to a specific node in the object graph. - The path might be either: +Specifies the path to a specific node in the object graph. +The path might be either: - * A dot-notation (`[String]`) literal or expression (as natively used with PowerShell) - * A array of strings (dictionary keys or Property names) and/or integers (list indices) - * A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object +* A dot-notation (`[String]`) literal or expression (as natively used with PowerShell) +* A array of strings (dictionary keys or Property names) and/or integers (list indices) +* A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object .PARAMETER Literal - If Literal switch is set, all (map) nodes in the given path are considered literal. +If Literal switch is set, all (map) nodes in the given path are considered literal. .PARAMETER ValueOnly - returns the value of the node instead of the node itself. +returns the value of the node instead of the node itself. .PARAMETER Unique - Specifies that if a subset of the nodes has identical properties and values, - only a single node of the subset should be selected. +Specifies that if a subset of the nodes has identical properties and values, +only a single node of the subset should be selected. .PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: +Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. +The failsafe will prevent infinitive loops for circular references as e.g. in: - $Test = @{Guid = New-Guid} - $Test.Parent = $Test + $Test = @{Guid = New-Guid} + $Test.Parent = $Test - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. +The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. +> [!Note] +> The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node +> at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" #> [Alias('gn')] @@ -3411,184 +3550,184 @@ function Get-Node { ) begin { - if ($Unique) { - # As we want to support case sensitive and insensitive nodes the unique nodes are matched by case - # also knowing that in most cases nodes are compared with its self. - $UniqueNodes = [System.Collections.Generic.Dictionary[String, System.Collections.Generic.HashSet[Object]]]::new() - } - $XdnPaths = @($Path).ForEach{ - if ($_ -is [XdnPath]) { $_ } - elseif ($literal) { [XdnPath]::new($_, $True) } - else { [XdnPath]$_ } - } -} - -process { - $Root = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Node = - if ($XdnPaths) { $XdnPaths.ForEach{ $Root.GetNode($_) } } - else { $Root } - if (-not $Unique -or $( - $PathName = $Node.Path.ToString() - if (-not $UniqueNodes.ContainsKey($PathName)) { - $UniqueNodes[$PathName] = [System.Collections.Generic.HashSet[Object]]::new() - } - $UniqueNodes[$PathName].Add($Node.Value) - )) { - if ($ValueOnly) { $Node.Value } else { $Node } - } -} -} -function Get-SortObjectGraph { -<# -.SYNOPSIS - Sort an object graph - -.DESCRIPTION - Recursively sorts a object graph. - -.PARAMETER InputObject - The input object that will be recursively sorted. - - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: - - ,$InputObject | Sort-Object. - -.PARAMETER PrimaryKey - Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject] - independent of the (descending) sort order. - - It is allowed to supply multiple primary keys. - -.PARAMETER MatchCase - (Alias `-CaseSensitive`) Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive. - -.PARAMETER Descending - Indicates that Sort-Object sorts the objects in descending order. The default is ascending order. - - > [!NOTE] - > Primary keys (see: [-PrimaryKey]) will always put on top. - -.PARAMETER MaxDepth - The maximal depth to recursively compare each embedded property (default: 10). -#> - -[Alias('Sort-ObjectGraph', 'sro')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Sort-ObjectGraph.md')][OutputType([Object[]])] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Alias('By')][String[]]$PrimaryKey, - - [Alias('CaseSensitive')] - [Switch]$MatchCase, - - [Switch]$Descending, - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) -begin { - $ObjectComparison = [ObjectComparison]0 - if ($MatchCase) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchCase'} - if ($Descending) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'Descending'} - # As the child nodes are sorted first, we just do a side-by-side node compare: - $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' - - $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - - function SortRecurse([PSCollectionNode]$Node, [PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { - $NodeList = $Node.GetNodeList() - for ($i = 0; $i -lt $NodeList.Count; $i++) { - if ($NodeList[$i] -is [PSCollectionNode]) { - $NodeList[$i] = SortRecurse $NodeList[$i] -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer - } - } - if ($Node -is [PSListNode]) { - $NodeList.Sort($PSListNodeComparer) - if ($NodeList.Count) { $Node.Value = @($NodeList.Value) } else { $Node.Value = @() } - } - else { # if ($Node -is [PSMapNode]) - $NodeList.Sort($PSMapNodeComparer) - $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) - foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) - if ($Node -is [PSObjectNode]) { $Node.Value = [PSCustomObject]$Properties } else { $Node.Value = $Properties } - } - $Node + if ($Unique) { + # As we want to support case sensitive and insensitive nodes the unique nodes are matched by case + # also knowing that in most cases nodes are compared with its self. + $UniqueNodes = [System.Collections.Generic.Dictionary[String, System.Collections.Generic.HashSet[Object]]]::new() + } + $XdnPaths = @($Path).ForEach{ + if ($_ -is [XdnPath]) { $_ } + elseif ($literal) { [XdnPath]::new($_, $True) } + else { [XdnPath]$_ } } } process { - $Node = [PSNode]::ParseInput($InputObject, $MaxDepth) - if ($Node -is [PSCollectionNode]) { - $Node = SortRecurse $Node -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer + $Root = [PSNode]::ParseInput($InputObject, $MaxDepth) + $Node = + if ($XdnPaths) { $XdnPaths.ForEach{ $Root.GetNode($_) } } + else { $Root } + if (-not $Unique -or $( + $PathName = $Node.Path.ToString() + if (-not $UniqueNodes.ContainsKey($PathName)) { + $UniqueNodes[$PathName] = [System.Collections.Generic.HashSet[Object]]::new() + } + $UniqueNodes[$PathName].Add($Node.Value) + )) { + if ($ValueOnly) { $Node.Value } else { $Node } } - $Node.Value +} +} +function Get-SortObjectGraph { +<# +.SYNOPSIS + Sort an object graph + +.DESCRIPTION + Recursively sorts a object graph. + +.PARAMETER InputObject + The input object that will be recursively sorted. + + > [!NOTE] + > Multiple input object might be provided via the pipeline. + > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. + > To avoid a list of (root) objects to unroll, use the **comma operator**: + + ,$InputObject | Sort-Object. + +.PARAMETER PrimaryKey + Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject] + independent of the (descending) sort order. + + It is allowed to supply multiple primary keys. + +.PARAMETER MatchCase + (Alias `-CaseSensitive`) Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive. + +.PARAMETER Descending + Indicates that Sort-Object sorts the objects in descending order. The default is ascending order. + + > [!NOTE] + > Primary keys (see: [-PrimaryKey]) will always put on top. + +.PARAMETER MaxDepth + The maximal depth to recursively compare each embedded property (default: 10). +#> + +[Alias('Sort-ObjectGraph', 'sro')] +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')] +[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Sort-ObjectGraph.md')][OutputType([Object[]])] param( + + [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] + $InputObject, + + [Alias('By')][String[]]$PrimaryKey, + + [Alias('CaseSensitive')] + [Switch]$MatchCase, + + [Switch]$Descending, + + [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth +) +begin { + $ObjectComparison = [ObjectComparison]0 + if ($MatchCase) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchCase'} + if ($Descending) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'Descending'} + # As the child nodes are sorted first, we just do a side-by-side node compare: + $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' + + $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } + $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } + + function SortRecurse([PSCollectionNode]$Node, [PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { + $NodeList = $Node.GetNodeList() + for ($i = 0; $i -lt $NodeList.Count; $i++) { + if ($NodeList[$i] -is [PSCollectionNode]) { + $NodeList[$i] = SortRecurse $NodeList[$i] -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer + } + } + if ($Node -is [PSListNode]) { + $NodeList.Sort($PSListNodeComparer) + if ($NodeList.Count) { $Node.Value = @($NodeList.Value) } else { $Node.Value = @() } + } + else { # if ($Node -is [PSMapNode]) + $NodeList.Sort($PSMapNodeComparer) + $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) + foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) + if ($Node -is [PSObjectNode]) { $Node.Value = [PSCustomObject]$Properties } else { $Node.Value = $Properties } + } + $Node + } +} + +process { + $Node = [PSNode]::ParseInput($InputObject, $MaxDepth) + if ($Node -is [PSCollectionNode]) { + $Node = SortRecurse $Node -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer + } + $Node.Value } } function Import-ObjectGraph { <# .SYNOPSIS - Deserializes a PowerShell File or any object-graphs from PowerShell file to an object. +Deserializes a PowerShell File or any object-graphs from PowerShell file to an object. .DESCRIPTION - The `Import-ObjectGraph` cmdlet safely converts a PowerShell formatted expression contained by a file - to an object-graph existing of a mixture of nested arrays, hash tables and objects that contain a list - of strings and values. +The `Import-ObjectGraph` cmdlet safely converts a PowerShell formatted expression contained by a file +to an object-graph existing of a mixture of nested arrays, hash tables and objects that contain a list +of strings and values. .PARAMETER Path - Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. - Wildcard characters are permitted. +Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. +Wildcard characters are permitted. .PARAMETER LiteralPath - Specifies a path to one or more locations that contain a PowerShell the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. +Specifies a path to one or more locations that contain a PowerShell the object-graph. +The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. +If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell +PowerShell not to interpret any characters as escape sequences. .PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] +Defines which object types are allowed for the deserialization, see: [About language modes][2] - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. +* Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + `[String]`, `[Array]` or `[HashTable]`. +* Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any - other files, which usually concerns PowerShell (`.ps1`) files. +The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any +other files, which usually concerns PowerShell (`.ps1`) files. - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. +> [!Caution] +> +> In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, +> CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. +> +> Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. +> Verify that the class types in the expression are safe before instantiating them. In general, it is +> best to design your configuration expressions with restricted or constrained classes, rather than +> allowing full freeform expressions. .PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given list type. +If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or +denied type initializer will be converted to the given list type. .PARAMETER MapAs - If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given map (dictionary or object) type. +If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or +denied type initializer will be converted to the given map (dictionary or object) type. - The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and - a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that - support explicit type initiators. +The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and +a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that +support explicit type initiators. .PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. +Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('Import-Object', 'imo')] @@ -3645,37 +3784,37 @@ end { function Merge-ObjectGraph { <# .SYNOPSIS - Merges two object graphs into one +Merges two object graphs into one .DESCRIPTION - Recursively merges two object graphs into a new object graph. +Recursively merges two object graphs into a new object graph. .PARAMETER InputObject - The input object that will be merged with the template object (see: [-Template] parameter). +The input object that will be merged with the template object (see: [-Template] parameter). - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: +> [!NOTE] +> Multiple input object might be provided via the pipeline. +> The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. +> To avoid a list of (root) objects to unroll, use the **comma operator**: - ,$InputObject | Compare-ObjectGraph $Template. + ,$InputObject | Compare-ObjectGraph $Template. .PARAMETER Template - The template that is used to merge with the input object (see: [-InputObject] parameter). +The template that is used to merge with the input object (see: [-InputObject] parameter). .PARAMETER PrimaryKey - In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to - link the items or properties: if the PrimaryKey exists on both the [-Template] and the - [-InputObject] and the values are equal, the dictionary or PowerShell object will be merged. - Otherwise (if the key can't be found or the values differ), the complete dictionary or - PowerShell object will be added to the list. +In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to +link the items or properties: if the PrimaryKey exists on both the [-Template] and the +[-InputObject] and the values are equal, the dictionary or PowerShell object will be merged. +Otherwise (if the key can't be found or the values differ), the complete dictionary or +PowerShell object will be added to the list. - It is allowed to supply multiple primary keys where each primary key will be used to - check the relation between the [-Template] and the [-InputObject]. +It is allowed to supply multiple primary keys where each primary key will be used to +check the relation between the [-Template] and the [-InputObject]. .PARAMETER MaxDepth - The maximal depth to recursively compare each embedded node. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). +The maximal depth to recursively compare each embedded node. +The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). #> [Alias('Merge-Object', 'mgo')] @@ -3771,10 +3910,10 @@ The schema object has the following major features: * Independent of the object notation (as e.g. [Json (JavaScript Object Notation)][2] or [PowerShell Data Files][3]) * Each test node is at the same level as the input node being validated -* Complex node requirements (as mutual exclusive nodes) might be selected using a logical formula +* Complex node Conditions (as mutual exclusive nodes) might be selected using a logical formula .EXAMPLE -#Test whether a `$Person` object meats the schema requirements. +#Test whether a `$Person` object meats the schema Conditions. $Person = [PSCustomObject]@{ FirstName = 'John' @@ -3845,719 +3984,915 @@ If set, the cmdlet will stop at the first invalid node and return the test resul .PARAMETER Elaborate -If set, the cmdlet will return the test result object for all tested nodes, even if they are valid +If set, the cmdlet will return the test result object for all nodes, even if they are valid or ruled out in a possible list node branch selection. -.PARAMETER AssertTestPrefix +.PARAMETER AssertPrefix -The prefix used to identify the assert test nodes in the schema object. By default, the prefix is `AssertTestPrefix`. +The prefix used to identify the Assert nodes in the schema object. By default, the prefix is `@`. .PARAMETER MaxDepth -The maximal depth to recursively test each embedded node. +The Maximum depth to recursively test each embedded node. The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). .LINK [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/SchemaObject.md "Schema object definitions" - #> +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ContainsOptionalTests', Justification = 'https://github.com/PowerShell/PSScriptAnalyzer/issues/1163')] [Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] +[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri = 'https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] +param( + [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] $InputObject, - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] + [Parameter(Mandatory = $true, Position = 0)] $SchemaObject, - [Parameter(ParameterSetName='ValidateOnly')] + [Parameter(ParameterSetName = 'ValidateOnly')] [Switch]$ValidateOnly, - [Parameter(ParameterSetName='ResultList')] + [Parameter(ParameterSetName = 'ResultList')] [Switch]$Elaborate, - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [ValidateNotNullOrEmpty()][String]$AssertTestPrefix = 'AssertTestPrefix', + [ValidateNotNullOrEmpty()][String]$AssertPrefixNodeName = 'AssertPrefix', - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth ) begin { - $Script:Yield = { - $Name = "$Args" -Replace '\W' - $Value = Get-Variable -Name $Name -ValueOnly -ErrorAction SilentlyContinue - if ($Value) { "$args" } - } - - $Script:Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } + $Script:UniqueCollections = @{} # The maximum schema object depth is bound by the input object depth (+1 one for the leaf test definition) $SchemaNode = [PSNode]::ParseInput($SchemaObject, ($MaxDepth + 2)) # +2 to be safe - $Script:AssertPrefix = if ($SchemaNode.Contains($AssertTestPrefix)) { $SchemaNode.Value[$AssertTestPrefix] } else { '@' } + $Script:AssertPrefix = if ($SchemaNode.Contains($AssertPrefixNodeName)) { $SchemaNode.Value[$AssertPrefixNodeName] } else { '@' } - function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } + class CheckBox { + [Nullable[Bool]]$NullableBool - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$([String]$SchemaNode) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object + CheckBox([Nullable[Bool]]$NullableBool) { $this.NullableBool = $NullableBool } + + [string] ToString() { + return $( + switch ($this.NullableBool) { + $null { [CommandColor][InverseColor]'[?]' } + $false { [ErrorColor][InverseColor]'[X]' } + $true { [VariableColor][InverseColor]'[V]' } + } + ) + } } - $Script:Tests = @{ - Description = 'Describes the test node' - References = 'Contains a list of assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Required = 'The node is required' - Unique = 'The node is unique' - - Minimum = 'The value is greater than or equal to' - ExclusiveMinimum = 'The value is greater than' - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - - MinimumLength = 'The value length is greater than or equal to' - Length = 'The value length is equal to' - MaximumLength = 'The value length is less than or equal to' - - MinimumCount = 'The node count is greater than or equal to' - Count = 'The node count is equal to' - MaximumCount = 'The node count is less than or equal to' - - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' + class Permutations : IEnumerator { + # This class enumerates all permutations of $n elements that Permutation over $i containers - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - AllowExtraNodes = 'Allow extra nodes' - } + [int[]] $Element + [List[int][]] $Container - $At = @{} - $Tests.Get_Keys().Foreach{ $At[$_] = "$($AssertPrefix)$_" } + Permutations([int]$ElementCount, [int]$ContainerCount) { + $this.Element = [int[]]::new($ElementCount) + $this.Container = [List[int][]]::new($ContainerCount) + } + + [object]get_Current() { + if ($null -eq $this.Container[0]) { return @() } + return $this.Container + } + + [bool] MoveNext() { + if ($this.Element.Count -eq 0 -or $this.Container.Count -eq 0) { return $false } + if ($null -eq $this.Container[0]) { + # First iteration + $this.Container[0] = [List[int]] (0..($this.Element.Count - 1)) + for ($i = 1; $i -lt $this.Container.Count; $i++) { $this.Container[$i] = [List[int]]::new() } + return $true + } + $n = 0 + while ($n -lt $this.Element.Count) { + $i = $this.Element[$n] # Container index + if (-not $this.Container[$i].Remove($n)) { throw "Container $i ($($this.Container[$i])) doesn't contain $n." } + $Carry = ++$i -ge $this.Container.Count + if ($Carry) { $i = 0 } + $this.Element[$n] = $i + $this.Container[$i].Add($n) + if (-not $Carry) { return $true } + $n++ + } + $this.Reset() + return $false + } + + [List[int][]] Copy() { + $Copy = [List[int][]]::new($this.Container.Count) + for ($i = 0; $i -lt $this.Container.Count; $i++) { + $Copy[$i] = [List[int]]::new($this.Container[$i]) + } + return $Copy + } - function ResolveReferences($Node) { - if ($Node.Cache.ContainsKey('TestReferences')) { return } + [void] Reset() { + $this.Element.Clear() + $this.Container.Clear() + } + [void] Dispose() {} } - function GetReference($LeafNode) { - $TestNode = $LeafNode.ParentNode - $References = if ($TestNode) { - if (-not $TestNode.Cache.ContainsKey('TestReferences')) { - $Stack = [Stack]::new() - while ($true) { - $ParentNode = $TestNode.ParentNode - if ($ParentNode -and -not $ParentNode.Cache.ContainsKey('TestReferences')) { - $Stack.Push($TestNode) - $TestNode = $ParentNode - continue - } - $RefNode = if ($TestNode.Contains($At.References)) { $TestNode.GetChildNode($At.References) } - $TestNode.Cache['TestReferences'] = [HashTable]::new($Ordinal[[Bool]$RefNode.CaseMatters]) - if ($RefNode) { - foreach ($ChildNode in $RefNode.ChildNodes) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($ChildNode.Name)) { - $TestNode.Cache['TestReferences'][$ChildNode.Name] = $ChildNode - } - } - } - $ParentNode = $TestNode.ParentNode - if ($ParentNode) { - foreach ($RefName in $ParentNode.Cache['TestReferences'].get_Keys()) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($RefName)) { - $TestNode.Cache['TestReferences'][$RefName] = $ParentNode.Cache['TestReferences'][$RefName] - } - } - } - if ($Stack.Count -eq 0) { break } - $TestNode = $Stack.Pop() + class Permutation : Permutations , IEnumerator[int] { + Permutation([int]$ElementCount, [int]$ContainerCount) : base($ElementCount, $ContainerCount) {} + [Int]get_Current() { return $this } + [string]ToString() { + return $( + foreach ($Indices in $this.Container) { + $Indices -join ',' } + ) -join '|' + } + } + + class TestStage { + static [Bool]$Debug + + [PSNode]$ObjectNode + [Bool]$Report + [Bool]$Elaborate + [Int]$Depth + [TestStage]$ParentStage + [Bool]$Passed = $true + [Nullable[Bool]]$CaseSensitive + [Int]$FailCount + [List[Object]]$Results + + TestStage([PSNode]$ObjectNode, [Bool]$Elaborate, [Bool]$Report, [int]$Depth) { + if ($Elaborate -and -not $Report) { throw "Can't elaborate in validate-only mode" } + $this.ObjectNode = $ObjectNode + $this.Elaborate = $Elaborate + $this.Report = $Report + $this.Depth = $Depth + if ([TestStage]::Debug) { $this.WriteDebug($null, $null) } + } + + [TestStage]Create([PSNode]$Node, [bool]$Scan) { + $TestStage = [TestStage]::new($Node, $this.Elaborate, $this.Report, ($this.Depth + 1)) + $TestStage.ParentStage = $this.ParentStage + $TestStage.CaseSensitive = $this.CaseSensitive + if (-not $this.Report) { return $TestStage } # Validate mode: output no results + # if ($this.Elaborate) { return $TestStage } # Elaborate mode: output all results + if ($Scan) { $TestStage.Results = [List[Object]]::new() } # (Re)start scan stage + elseif ($this.Results -is [IList]) { $TestStage.Results = $this.Results } # Add scan results to parent + return $TestStage + } + + hidden WriteDebug($TestNode, [String]$Issue) { + $Indent = ' ' * ($this.Depth * 2) + $Line = (Get-PSCallStack).Where({ $_.Command }, 'First').ScriptLineNumber + $Prompt = if ($this.Report) { 'Report' } else { 'Validate' } + if ($null -ne $this.Results) { $Prompt += '(Scan)' } + $Node = $this.ObjectNode + if ($TestNode -is [PSNode]) { + Write-Host "$Indent$($Line):$Prompt>$($Node)?$($TestNode.Name)=$TestNode" ([CheckBox]$this.passed) $Issue } - $TestNode.Cache['TestReferences'] - } else { @{} } - if ($References.Contains($LeafNode.Value)) { - $AssertNode.Cache['TestReferences'] = $References - $References[$LeafNode.Value] - } - else { SchemaError "Unknown reference: $LeafNode" $ObjectNode $LeafNode } - } - - function MatchNode ( - [PSNode]$ObjectNode, - [PSNode]$TestNode, - [Switch]$ValidateOnly, - [Switch]$Elaborate, - [Switch]$Ordered, - [Nullable[Bool]]$CaseSensitive, - [Switch]$MatchAll, - $MatchedNames - ) { - $Violates = $null - $Name = $TestNode.Name + elseif ($Issue) { + Write-Host "$Indent$($Line):$Prompt>$($Node.Path)=$Issue" + } + else { + Write-Host "$Indent$($Line):$Prompt>$($Node.Path)=$Node" + } + } - $ChildNodes = $ObjectNode.ChildNodes - if ($ChildNodes.Count -eq 0) { return } + [Object]Check([PSNode]$TestNode, [String]$Issue, [Bool]$Passed) { + if (-not $Passed) { + $this.Passed = $false + $this.FailCount++ + } + if ([TestStage]::Debug) { $this.WriteDebug($TestNode, $Issue) } + if (-not $this.Elaborate -and ($Passed -or -not $this.Report)) { return @() } + $Result = [PSCustomObject]@{ + ObjectNode = $this.ObjectNode + SchemaNode = $TestNode + Valid = $Passed + Issue = $Issue + } + $Result.PSTypeNames.Insert(0, 'TestResult') + if ($null -eq $this.Results) { return $Result } + $this.Results.Add($Result) + return @() # return nothing (enumerable null) + } - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } + [Object]AddResults ([List[Object]]$Results) { + if (-not $Results) { return @() } + if ($null -eq $this.Results) { return $Results } + $this.Results.AddRange($Results) + return @() + } - if ($ObjectNode -is [PSMapNode] -and $TestNode.NodeOrigin -eq 'Map') { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "The node $Name is not in order" + hidden [String]GroupDesignate($Stages, $TestIndex, $Permutation) { + return $( + if ($Permutation[$TestIndex].Count -eq 0) { [CheckBox]::new($false) } + else { + foreach ($NodeIndex in $Permutation[$TestIndex]) { + $Check = if ($Stages[$NodeIndex] -and $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $Stages[$NodeIndex][$TestIndex].Passed + } + "$($this.ObjectNode.ChildNodes[$NodeIndex])$([CheckBox]::new($Check))" + } } - } else { $ChildNode = $false } + ) -join ',' } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Expected at least $($TestNodes.Count) (ordered) nodes" + + [iDictionary]GetDesignates ($Stages, $SubTests, $Permutation, $TestPassed) { + $Dictionary = [Dictionary[String, String]]::new() + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + $Name = $SubTests[$TestIndex].Name + $Dictionary[$Name] = [CheckBox]::new($TestPassed[$TestIndex]).ToString() + '=' + + $this.GroupDesignate($Stages, $TestIndex, $Permutation) } - $ChildNode = $ChildNodes[$NodeIndex] + return $Dictionary } - else { $ChildNode = $null } - if ($Violates) { - if (-not $ValidateOnly) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $AssertNode - Valid = -not $Violates - Issue = $Violates + [iList]ListDesignates ($Stages, $SubTests, $Permutation, $TestPassed) { + return @( + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + if ($TestPassed.ContainsKey($TestIndex)) { continue } + $SubTests[$TestIndex].Name + + [CheckBox]::new($TestPassed[$TestIndex]).ToString() + '=' + + $this.GroupDesignate($Stages, $TestIndex, $Permutation) } - $Output.PSTypeNames.Insert(0, 'TestResult') - $Output - } - return + ) } - if ($ChildNode -is [PSNode]) { - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Issue + + [String]Casing() { if ($this.CaseSensitive) { return '(case sensitive) ' } else { return '' } } + } + + [TestStage]::Debug = $DebugPreference -in 'Stop', 'Continue', 'Inquire' + + function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { + if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } + elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } + $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) + } + + function SchemaError($Message, $SchemaNode) { + $Exception = [ArgumentException]"$([String]$SchemaNode) $Message" + StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $SchemaNode + } + + $Script:Asserts = @{ + Description = 'Describes the test node' + References = 'Contains a list of Assert references' + CaseSensitive = 'The (descendant) nodes are considered case sensitive' + Unique = '_ObjectValue_ is unique' + + Ordered = 'The nodes are in order' + AnyName = 'Any map name is allowed' + Requires = 'The node requirement is met' + + Optional = 'The map node is optional' + Count = 'The list node exists _SchemaValue_ times' + MinimumCount = 'The list node exists at least _SchemaValue_ times' + MaximumCount = 'The list node exists at most _SchemaValue_ times' + + Type = '_ObjectValue_ is of type _SchemaValue_' + NotType = '_ObjectValue_ is not type _SchemaValue_' + + Minimum = '_ObjectValue_ is greater than or equal to _SchemaValue_' + ExclusiveMinimum = '_ObjectValue_ is greater than _SchemaValue_' + ExclusiveMaximum = '_ObjectValue_ is less than _SchemaValue_' + Maximum = '_ObjectValue_ is less than or equal to _SchemaValue_' + + MinimumLength = '_ObjectValue_ length is greater than or equal to _SchemaValue_' + Length = '_ObjectValue_ length is equal to _SchemaValue_' + MaximumLength = '_ObjectValue_ length is less than or equal to _SchemaValue_' + + Like = '_ObjectValue_ is like _SchemaValue_' + Match = '_ObjectValue_ matches _SchemaValue_' + NotLike = '_ObjectValue_ is not like _SchemaValue_' + NotMatch = '_ObjectValue_ not matches _SchemaValue_' + } + + $At = @{} + $Asserts.Get_Keys().Foreach{ $At[$_] = "$($Script:AssertPrefix)$_" } + + function GetLink($LeafNode) { + # A test node with a string value is a reference to another node + # described in a ancestor @Reference map node + $ParentNode = $LeafNode.ParentNode + if (-not $ParentNode.Cache.ContainsKey('@References')) { + $Stack = [Stack]::new() + while ($ParentNode -and -not $ParentNode.Cache.ContainsKey('@References')) { + $Stack.Push($ParentNode) + $ParentNode = $ParentNode.ParentNode } - TestNode @TestParams - if (-not $Issue) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - $SingleIssue = $Null - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Issue - } - TestNode @TestParams - if($Issue) { - if ($Elaborate) { $Issue } - elseif (-not $ValidateOnly -and $MatchAll) { - if ($null -eq $SingleIssue) { $SingleIssue = $Issue } else { $SingleIssue = $false } + $References = if ($ParentNode -and $ParentNode.Cache.ContainsKey('@References')) { $ParentNode.Cache['@References'] } else { @{} } + while ($Stack.Count) { + $ParentNode = $Stack.Pop() + if ($ParentNode.Contains($At.References)) { + $Inherit = $References + $RefNode = $ParentNode.GetChildNode($At.References) + if ($RefNode -is [PSMapNode]) { + $Ordinal = if ($RefNode.CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + $References = [HashTable]::new($Ordinal) + foreach ($Node in $RefNode.ChildNodes) { $References[$Node.Name] = $Node } + } + else { SchemaError "The reference node should be a map node" $RefNode } + foreach ($Key in $Inherit.get_Keys()) { + if (-not $References.ContainsKey($Key)) { $References[$Key] = $Inherit[$Key] } } } - else { - $null = $MatchedNames.Add($ChildNode.Name) - if (-not $MatchAll) { break } - } + $ParentNode.Cache['@References'] = $References } - if ($SingleIssue) { $SingleIssue } } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } + $Reference = $ParentNode.Cache['@References'][$LeafNode.Value] + if ($Reference) { $Reference } else { SchemaError "Unknown reference: $LeafNode" $LeafNode } } function TestNode ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$Elaborate, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity frm the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node + [TestStage]$TestStage, + [PSCollectionNode]$SchemaNode ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ParameterColor]'Caller (line: $($Caller.ScriptLineNumber))'):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ParameterColor]'ObjectNode:')" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ParameterColor]'SchemaNode:')" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ParameterColor]'ValidateOnly:')" ([Bool]$ValidateOnly) - } - if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # Allow any node + $ObjectNode = $TestStage.ObjectNode + if ($ObjectNode -is [PSLeafNode]) { $SubNodes = @() } else { $SubNodes = $ObjectNode.ChildNodes } + if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # @() = Allow any node, @{} = Deny any node $AssertValue = $ObjectNode.Value - $RefInvalidNode.Value = $null # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} # $AssertNodes{] = $ChildNodes.@ + $ExtraTest, $Condition, $Ordered, $CaseMatters, $Ordinal = $null + $CaseMatters = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } + $Ordinal = if ($CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + $SubTests = [OrderedDictionary]::new($Ordinal) + $AssertNodes = [Ordered]@{} # $AssertNodes[] = $SubNodes.@ if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() foreach ($Node in $SchemaNode.ChildNodes) { - if ($Null -eq $Node.Parent -and $Node.Name -eq $AssertTestPrefix) { continue } - if ($Node.Name.StartsWith($AssertPrefix)) { - $TestName = $Node.Name.SubString($AssertPrefix.Length) - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown assert: '$($Node.Name)'" $ObjectNode $SchemaNode } + if ($Null -eq $Node.ParentNode.ParentNode -and $Node.Name -eq $AssertPrefixNodeName) { continue } + if ($Node.Name -is [String] -and $Node.Name.StartsWith($Script:AssertPrefix)) { + $TestName = $Node.Name.SubString($Script:AssertPrefix.Length) + if ($TestName -notin $Asserts.Keys) { SchemaError "Unknown Assert: '$TestName'" $SchemaNode } $AssertNodes[$TestName] = $Node } - else { $TestNodes.Add($Node) } + else { + $SubTests[[Object]$Node.Name] = if ($Node -is [PSLeafNode]) { GetLink $Node } else { $Node } + } } } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - if ($AssertNodes.Contains('CaseSensitive')) { $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] } - $AllowExtraNodes = if ($AssertNodes.Contains('AllowExtraNodes')) { $AssertNodes['AllowExtraNodes'] } + elseif ($SchemaNode -is [PSListNode]) { + foreach ($Node in $SchemaNode.ChildNodes) { $SubTests[[Object]$Node.Name] = $Node } + } -#Region Node validation + if ($AssertNodes.Contains('CaseSensitive')) { $TestStage.CaseSensitive = $AssertNodes['CaseSensitive'] } - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null + $LeafTest = $false foreach ($TestName in $AssertNodes.get_Keys()) { - $AssertNode = $AssertNodes[$TestName] - $Criteria = $AssertNode.Value - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Description') { $Null } - elseif ($TestName -eq 'References') { } + + $TestNode = $AssertNodes[$TestName] + $TestValue = $TestNode.Value + + #Region Node Asserts + + if ($TestName -in 'Description', 'References', 'Optional', 'Count', 'MinimumCount', 'MaximumCount') { + continue + } + elseif ($TestName -eq 'CaseSensitive') { + if ($null -ne $TestValue -and $TestValue -isnot [Bool]) { + SchemaError "The case sensitivity value should be a boolean: $TestValue" $SchemaNode + } + continue + } + elseif ($TestName -eq 'Ordered') { + if ($TestValue -is [Bool]) { $Ordered = [Bool]$TestValue } + else { SchemaError "The ordered assert should be a boolean" $SchemaNode } + if ($ObjectNode -isnot [PSCollectionNode]) { + $TestStage.Check($TestNode, "The $ObjectNode is not a collection node", $false) + } + continue + } + elseif ($TestName -eq 'AnyName') { + $ExtraTest = $TestNode + if (-not $SubTests) { [OrderedDictionary]::new() } + continue + } + elseif ($TestName -eq 'Requires') { + if ($Ordered) { SchemaError "A ordered collection cannot have a Requires assert" $SchemaNode } + if ($ObjectNode -is [PSCollectionNode]) { + $Condition = [LogicalFormula]::new() + $TestValue.foreach{ $Condition.And([LogicalFormula]$_) } # 'a or b', 'c or d' --> 'a or b and (c or d)' + } + else { $TestStage.Check($TestNode, "The node $ObjectNode is not a collection node", $false) } + continue + } elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { + $TypeName = $null + $FoundType = foreach ($TypeName in $TestValue) { if ($TypeName -in $null, 'Null', 'Void') { if ($null -eq $AssertValue) { $true; break } } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } + else { + $Type = if ($TypeName -is [Type]) { $TypeName } else { $TypeName -as [Type] } + if (-not $Type) { SchemaError "Unknown type: $TypeName" $TypeName } + if ($ObjectNode -is $Type -or $AssertValue -is $Type) { $true; break } } - if ($ObjectNode -is $Type -or $AssertValue -is $Type) { $true; break } } $Not = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - if ($null -eq $FoundType -xor $Not) { $Violates = "The node or value is $(if (!$Not) { 'not ' })of type $AssertNode" } - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "The case sensitivity value should be a boolean: $Criteria" $ObjectNode $SchemaNode + if ($null -eq $FoundType -xor $Not) { + $DisplayType = $([TypeColor][PSSerialize]::new($TypeName, [PSLanguageMode]'NoLanguage')) + $TestStage.Check($TestNode, "$($ObjectNode.DisplayValue) is $(if (!$Not) { 'not ' })of type $DisplayType", $false) } } elseif ($TestName -in 'Minimum', 'ExclusiveMinimum', 'ExclusiveMaximum', 'Maximum') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "The $ObjectNode is not a string or value type", $false) } elseif ($TestName -eq 'Minimum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cle $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ile $Value } + else { $TestValue -le $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less or equal than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())less or equal than $TestValue", $false) } } elseif ($TestName -eq 'ExclusiveMinimum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -clt $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ilt $Value } + else { $TestValue -lt $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())less than $TestValue", $false) } } elseif ($TestName -eq 'ExclusiveMaximum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cgt $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -igt $Value } + else { $TestValue -gt $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())greater than $TestValue", $false) } } - else { # if ($TestName -eq 'Maximum') { + else { + # if ($TestName -eq 'Maximum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cge $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ige $Value } + else { $TestValue -ge $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())greater or equal than $TestValue)", $false) } } - if ($Violates) { break } + if (-not $TestStage.Passed) { break } } } - elseif ($TestName -in 'MinimumLength', 'Length', 'MaximumLength') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "The $ObjectNode is not a string or value type", $false) break } $Length = "$Value".Length if ($TestName -eq 'MinimumLength') { - if ($Length -lt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is less than $AssertNode" + if ($Length -lt $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is less than $TestValue", $false) } } elseif ($TestName -eq 'Length') { - if ($Length -ne $Criteria) { - $Violates = "The string length of '$Value' ($Length) is not equal to $AssertNode" + if ($Length -ne $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is not equal to $TestValue", $false) } } - else { # if ($TestName -eq 'MaximumLength') { - if ($Length -gt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is greater than $AssertNode" + else { + # if ($TestName -eq 'MaximumLength') { + if ($Length -gt $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is greater than $TestValue", $false) } } - if ($Violates) { break } + if (-not $TestStage.Passed) { break } } } elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $Negate = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - $Match = $TestName.EndsWith('Match', 'OrdinalIgnoreCase') + $Match = $TestName.EndsWith('Match', 'OrdinalIgnoreCase') $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "$ObjectNode is not a string or value type", $false) break } $Found = $false - foreach ($AnyCriteria in $Criteria) { + $Criteria = $null + foreach ($Criteria in $TestValue) { $Found = if ($Match) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } + if ($true -eq $TestStage.CaseSensitive) { $Value -cmatch $Criteria } + elseif ($false -eq $TestStage.CaseSensitive) { $Value -imatch $Criteria } + else { $Value -match $Criteria } } - else { # if ($TestName.EndsWith('Link', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } + else { + if ($true -eq $TestStage.CaseSensitive) { $Value -clike $Criteria } + elseif ($false -eq $TestStage.CaseSensitive) { $Value -ilike $Criteria } + else { $Value -like $Criteria } } if ($Found) { break } } $IsValid = $Found -xor $Negate if (-not $IsValid) { - $Not = if (-Not $Negate) { ' not' } - $Violates = - if ($Match) { "The $(&$Yield '(case sensitive) ')value $Value does$not match $AssertNode" } - else { "The $(&$Yield '(case sensitive) ')value $Value is$not like $AssertNode" } - } - } - } - - elseif ($TestName -in 'MinimumCount', 'Count', 'MaximumCount') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The node $ObjectNode is not a collection node" - } - elseif ($TestName -eq 'MinimumCount') { - if ($ChildNodes.Count -lt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is less than $AssertNode" - } - } - elseif ($TestName -eq 'Count') { - if ($ChildNodes.Count -ne $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is not equal to $AssertNode" - } - } - else { # if ($TestName -eq 'MaximumCount') { - if ($ChildNodes.Count -gt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is greater than $AssertNode" + $Not = if (-not $Negate) { ' not' } + $DisplayCriteria = $([TypeColor][PSSerialize]::new($Criteria, [PSLanguageMode]'NoLanguage')) + if ($Match) { $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) does$not $($TestStage.Casing())match $DisplayCriteria", $false) } + else { $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is$not $($TestStage.Casing())like $DisplayCriteria", $false) } } } } - - elseif ($TestName -eq 'Required') { } - elseif ($TestName -eq 'Unique' -and $Criteria) { + elseif ($TestName -eq 'Unique') { + if (-not $TestValue) { continue } if (-not $ObjectNode.ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode + SchemaError "The unique Assert can't be used on a root node" $SchemaNode } - if ($Criteria -eq $true) { $UniqueCollection = $ObjectNode.ParentNode.ChildNodes } - elseif ($Criteria -is [String]) { - if (-not $UniqueCollections.Contains($Criteria)) { - $UniqueCollections[$Criteria] = [List[PSNode]]::new() + if ($TestValue -eq $true) { $UniqueCollection = $ObjectNode.ParentNode.ChildNodes } + elseif ($TestValue -is [String]) { + if (-not $Script:UniqueCollections.Contains($TestValue)) { + $Script:UniqueCollections[$TestValue] = [List[PSNode]]::new() } - $UniqueCollection = $UniqueCollections[$Criteria] + $UniqueCollection = $Script:UniqueCollections[$TestValue] } - else { SchemaError "The unique assert value should be a boolean or a string" $ObjectNode $SchemaNode } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) + else { SchemaError "The unique assert value should be a boolean or a string" $SchemaNode } + $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$TestStage.CaseSensitive) foreach ($UniqueNode in $UniqueCollection) { if ([object]::ReferenceEquals($ObjectNode, $UniqueNode)) { continue } # Self if ($ObjectComparer.IsEqual($ObjectNode, $UniqueNode)) { - $Violates = "The node is equal to the node: $($UniqueNode.Path)" + $TestStage.Check($TestNode, "The $($ObjectNode.DisplayValue) is equal to the node: $($UniqueNode.Path)", $false) break } } - if ($Criteria -is [String]) { $UniqueCollection.Add($ObjectNode) } - } - elseif ($TestName -eq 'AllowExtraNodes') {} - elseif ($TestName -in 'Ordered', 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' is not a collection node" - } + if ($TestValue -is [String]) { $UniqueCollection.Add($ObjectNode) } } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } + else { SchemaError "Unhandled Assert: $TestName" $TestNode } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } + #EndRegion Node Asserts - if ($Violates -or $Elaborate) { - $Issue = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName]) } - else { "$($Tests[$TestName] -replace 'The value ', "The value $ObjectNode ") $AssertNode" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = $Issue - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } + if (-not $TestStage.Passed) { return } # Already issued + if ([TestStage]::Debug -or $TestStage.Elaborate) { + $Issue = $Asserts[$TestName] -replace '\b_ObjectValue_\b', $ObjectNode.DisplayValue -replace '\b_SchemaValue_\b', $TestNode.DisplayValue + $TestStage.Check($TestNode, $Issue, $true) } } -#EndRegion Node validation + if ($LeafTest) { return } # No child nodes to test - if ($Violates) { return } + #Region SubTests -#Region Required nodes - - $ChildNodes = $ObjectNode.ChildNodes - - if ($TestNodes.Count -and -not $AssertNodes.Contains('Type')) { - if ($SchemaNode -is [PSListNode] -and $ObjectNode -isnot [PSListNode]) { - $Violates = "The node $ObjectNode is not a list node" + if (-not $SubTests.Count) { + if ($Condition) { SchemaError "Expected a test node for each requirement" $SchemaNode } + if (-not $SubNodes) { return } + } + $FailCount = $TestStage.FailCount + $CaseMatters = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } + $Ordinal = if ($CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + + $Required = if ($null -eq $Condition) { [LogicalFormula]::new() } + $RequiredNames = [HashSet[String]]::new($Ordinal) + if ($Condition) { $Condition.Find({ $Args[0] -is [LogicalVariable] }, $true).foreach{ $null = $RequiredNames.Add($_.Value) } } + + $ContainsOptionalTests = $false + $TestIndices = [Dictionary[string, int]]::new($Ordinal) + $MinimumCount = [Dictionary[int, int]]::new() + $MaximumCount = [Dictionary[int, int]]::new() + $TestIndex = 0 + foreach ($TestName in $SubTests.get_Keys()) { + #$TestName is the reference name, $SubTest.Name is the actual name of the test + $SubTest = $SubTests[$TestIndex] + $TestIndices[$SubTest.Name] = $TestIndex + $Optional = $SubTest.GetValue($At.Optional, $null) + $Count = $SubTest.GetValue($At.Count, $null) + $Minimum = $SubTest.GetValue($At.MinimumCount, $null) + $Maximum = $SubTest.GetValue($At.MaximumCount, $null) + if ($Optional -and ($Count -or $Minimum)) { + SchemaError "The Optional (for maps) and (Minimum)Count (for lists) asserts are mutual exclusive" $SubTest + } + elseif ($Count -and ($Minimum -or $Maximum)) { + SchemaError "The count and MinimumCount/MaximumCount asserts are mutual exclusive" $SubTest } - if ($SchemaNode -is [PSMapNode] -and $ObjectNode -isnot [PSMapNode]) { - $Violates = "The node $ObjectNode is not a map node" + if ($null -ne $Optional) { + if ($null -ne ($Bool = $Optional -as [Bool])) { + $MinimumCount[$TestIndex] = 1 - $Bool + } + else { SchemaError "The Optional assert should be a boolean type" $SubTest } + } + if ($null -ne $Count) { + if ($null -ne ($Int = $Count -as [UInt32])) { + $MinimumCount[$TestIndex] = $int + $MaximumCount[$TestIndex] = $int + } + else { SchemaError "The MinimumCount assert should be a positive integer type" $SubTest } + } + if ($null -ne $Minimum) { + if ($null -ne ($Int = $Minimum -as [UInt32])) { + $MinimumCount[$TestIndex] = $int + } + else { SchemaError "The MinimumCount assert should be a positive integer type" $SubTest } + } + if ($null -ne $Maximum) { + if ($null -ne ($Int = $Maximum -as [UInt32])) { + $MaximumCount[$TestIndex] = $int + } + else { SchemaError "The MaximumCount assert should be a positive integer type" $SubTest } + } + if ($MinimumCount.ContainsKey($TestIndex)) { + if ($MinimumCount[$TestIndex]) { + if ($Required) { $Required.And($TestName) } + elseif ($RequiredNames.Add($TestName)) { $Condition.And($TestName) } + } + elseif ($Condition) { SchemaError "Required tests cannot be optional" $SubTest } + } + elseif ($Required) { + $MinimumCount[$TestIndex] = 1 + if ($RequiredNames.Add($TestName)) { $Required.And($TestName) } } + elseif ($MinimumCount.ContainsKey($TestIndex)) { + $ContainsOptionalTests = $MinimumCount[$TestIndex] -eq 0 + } + else { $MinimumCount[$TestIndex] = 0 } + $TestIndex++ } - - if (-Not $Violates) { - $RequiredNodes = $AssertNodes['RequiredNodes'] - $CaseSensitiveNames = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } - $AssertResults = [HashTable]::new($Ordinal[[Bool]$CaseSensitiveNames]) - - if ($RequiredNodes) { $RequiredList = [List[Object]]$RequiredNodes.Value } else { $RequiredList = [List[Object]]::new() } - foreach ($TestNode in $TestNodes) { - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } - if ($AssertNode -is [PSMapNode] -and $AssertNode.GetValue($At.Required)) { $RequiredList.Add($TestNode.Name) } + if ($Condition) { $Required = $Condition } + # else { $Required.Find({ $Args[0] -is [LogicalVariable] }, $true).foreach{ $null = $RequiredNames.Add($_.Value) } } + if ($Ordered) { + if ($SubNodes.Count -lt $SubTests.Count) { + $TestStage.Check($SchemaNode, "There are less than $($SubTests.Count) (ordered) child nodes", $false) + return + } + elseif ($SubNodes.Count -gt $SubTests.Count -and -not $ExtraTest) { + $TestStage.Check($SchemaNode, "There are more than $($SubTests.Count) (ordered) child nodes", $false) + return + } + if ($ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode]) { + for ($Index = 0; $Index -lt $SubTests.Count; $Index++) { + $NodeName = $SubNodes[$Index].Name + $TestName = $SubTests[$Index].Name + $EqualName = if ($CaseMatters) { $TestName -ceq $NodeName } else { $TestName -ieq $NodeName } + if (-not $EqualName) { + $TestStage.Check($SchemaNode, "Node #$Index ($([PSSerialize]$SubNodes[$Index].Name)) was not $([PSSerialize]$Name)", $false) + return + } + } + } + for ($Index = 0; $Index -lt $SubTests.Count; $Index++) { + $SubNode = $SubNodes[$Index] + $SubTest = $SubTests[$Index] + $ChildStage = $TestStage.Create($SubNode, $false) + TestNode $ChildStage $SubTest + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } + } + while ($Index -lt $SubNodes.Count) { + $SubNode = $SubNodes[$Index] + $ChildStage = $TestStage.Create($SubNode, $false) + TestNode $ChildStage $ExtraTest + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } + $Index++ + } + return + } + elseif ($ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode]) { + if ($SubNodes.Count -gt $SubTests.Count -and -not $ExtraTest) { + foreach ($ChildNode in $SubNodes) { + if ($SubTests.Contains($ChildNode.Name)) { continue } + $TestStage.Check($SchemaNode, "Node $([PSSerialize]$ChildNode.Name) is denied", $false) + } + return + } + if ($SubNodes) { $Names = $SubNodes.Name } else { $Names = @() } + if ($Required.Terms -and -not $Required.Evaluate($Names)) { + $TestStage.Check($SchemaNode, "The requirement { $Required } is not met", $false) + return + } + foreach ($ChildNode in $SubNodes) { + if ($SubTests.Contains($ChildNode.Name)) { $SubTest = $SubTests[[Object]$ChildNode.Name] } + elseif ($ExtraTest) { $SubTest = $ExtraTest } + else { + $TestStage.Check($SchemaNode, "Node $([PSSerialize]$ChildNode.Name) is denied", $false) + continue + } + $ChildStage = $TestStage.Create($ChildNode, $false) + TestNode $ChildStage $SubTest + if (-not $ChildStage.Passed) { $TestStage.Passed = $false } + } + return + } + # (unordered) $ObjectNode -is [PSListNode] -or $SchemaNode -is [PSListNode] + if ($ExtraTest) { $SubTests[[Object]'@AnyName'] = $ExtraTest } # The ExtraTest is just an optional test for a list + if ($SubTests.count -eq 1 -and $Required.Terms.count -le 1) { + # Single test (no scanning required) + $SubTest = $SubTests[0] + if ($SubNodes.Count -lt $MinimumCount[0]) { + $TestStage.Check($SubTest, "The node $ObjectNode contains less than $($MinimumCount[0]) child nodes", $false) + return } + if ($MaximumCount.ContainsKey(0) -and $SubNodes.Count -gt $MaximumCount[0]) { + $TestStage.Check($SubTest, "The node $ObjectNode contains more than $($MaximumCount[0]) child nodes", $false) + return + } + foreach ($ChildNode in $SubNodes) { + $ChildStage = $TestStage.Create($ChildNode, $false) + TestNode $ChildStage $SubTest + $TestStage.Passed = $ChildStage.Passed + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } # Validate only + } + return + } + $Stages = [Object[]]::new($SubNodes.Count) + $TestStage.Passed = $false + $Best = $null + foreach ($Permutation in [Permutation]::new($SubNodes.Count, $SubTests.Count)) { + $Score = 0 + $TestPassed = [Dictionary[Int, Bool]]::new() + $UsedNodes = [HashSet[Int]]::new() + $RequiredPassed = if ($Required.Terms) { + $Required.Evaluate({ + $TestIndex = $TestIndices[$_] + if ($null -eq $TestIndex) { return $true } # Might happen at maxdepth + $SubTest = $SubTests[$TestIndex] + $Indices = $Permutation[$TestIndex] + $OutsideBounds = if ($Indices.Count -lt $MinimumCount[$TestIndex]) { + $Indices.Count - $MinimumCount[$TestIndex] + } + elseif ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $MaximumCount[$TestIndex] - $Indices.Count + } + if ($OutsideBounds) { + $Score -= $OutsideBounds + if (-not $TestStage.Report) { return $false } # Validate only + # if (-not $TestPassed.ContainsKey($TestIndex)) { return $false } + foreach ($NodeIndex in $Indices) { # Add score based on what is known + if (-not $Stages[$NodeIndex]) { continue } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { continue } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount } + } + return $false + } + if (-not $TestPassed.ContainsKey($TestIndex)) { $TestPassed[$TestIndex] = $false } + foreach ($NodeIndex in $Indices) { + # A logical variable might refer to multiple child nodes + if (-not $Stages[$NodeIndex]) { $Stages[$NodeIndex] = [Dictionary[int, TestStage]]::new() } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $ChildNode = $SubNodes[$NodeIndex] + $Stages[$NodeIndex][$TestIndex] = $TestStage.Create($ChildNode, $true) + TestNode $Stages[$NodeIndex][$TestIndex] $SubTest + } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount; return $false } # All child nodes need to fulfill the test + $null = $UsedNodes.Add($NodeIndex) - foreach ($Requirement in $RequiredList) { - $LogicalFormula = [LogicalFormula]$Requirement - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $null - Operator = $null - Negate = $null + } + $Score += 1 + $Indices.Count + $TestPassed[$TestIndex] = $true + return $true }) - $Term, $Operand, $Accumulator = $null - While ($Stack.Count -gt 0) { - # Accumulator = Accumulator Operand - # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Operator = $Pop.Operator - if ($null -eq $Operator) { $Operand = $Pop.Accumulator } - else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } - $Negate = $Pop.Negate - $Compute = $null -notin $Operand, $Operator, $Accumulator - while ($Compute -or $Enumerator.MoveNext()) { - if ($Compute) { $Compute = $false} - else { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $SchemaNode.GetChildNode($Name) - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = $false - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - $AssertResults[$Name] = $MatchedNames.Count -gt $MatchCount0 - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term.Value -eq 'Not') { $Negate = -Not $Negate } - elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } - else { SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode } - } - elseif ($Term -is [LogicalFormula]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator, $Operator, $Negate = $null - $Enumerator = $Term.Terms.GetEnumerator() - continue - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - $Negate = $null - if ($Operator -eq 'And') { - $Operator = $null - if ($Accumulator -eq $false -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - $Operator = $null - if ($Accumulator -eq $true -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Operator = $null - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand = $Null + } else { $true } + $NodesLeft = $SubNodes.Count - $UsedNodes.Count + $TestsLeft = $SubTests.Count - $TestPassed.Count + $OptionalPassed = if (-not $RequiredPassed) { $false } + elseif ($NodesLeft -and $TestsLeft) { $null } # else: one or both are zero + elseif (-not $NodesLeft) { $true } + elseif (-not $ContainsOptionalTests) { $false } + # $TestOptional = if ($RequiredPassed -and $NodesLeft) { + # if ($TestsLeft) { $ContainsOptionalTests } else { $RequiredPassed = $false } + # } + # $OptionalPassed = $null + # if ($TestOptional) { + if ($null -eq $OptionalPassed) { + for ($TestIndex = 0; $TestIndex -lt $SubTests.Count; $TestIndex++) { + if ($TestPassed.ContainsKey($TestIndex)) { continue } + if ($MinimumCount[$TestIndex]) { continue } # not optional (handled in condition evaluation) + $Indices = $Permutation[$TestIndex] + if ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $Score -= $Indices.Count - $MaximumCount[$TestIndex] + if (-not $TestStage.Report) { break } # Validate only + foreach ($NodeIndex in $Indices) { # Add score based on what is known + if (-not $Stages[$NodeIndex]) { continue } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { continue } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount } } + $OptionalPassed = $false + break } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Operator" $ObjectNode $SchemaNode + foreach ($NodeIndex in $Indices) { + if (-not $Stages[$NodeIndex]) { $Stages[$NodeIndex] = [Dictionary[int, TestStage]]::new() } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $ChildNode = $SubNodes[$NodeIndex] + $Stages[$NodeIndex][$TestIndex] = $TestStage.Create($ChildNode, $true) + TestNode $Stages[$NodeIndex][$TestIndex] $SubTests[$TestIndex] # $SubTest ??? + } + $Stage = $Stages[$NodeIndex][$TestIndex] + $OptionalPassed = $Stage.Passed + if ($OptionalPassed) { $Score++ } else { $Score -= 2 + $Stage.FailCount; break } # All (optional) child nodes need to fulfill the test } + if ($OptionalPassed -eq $false) { break } + $Score += 1 + $Indices.Count } - if ($Accumulator -eq $False) { - $Violates = "The required node condition $LogicalFormula is not met" - break + } else { $Score -= $NodesLeft } + if ($null -eq $OptionalPassed) { $OptionalPassed = $true } + + if ([TestStage]::Debug) { + $Better = if (-not $Best -or $Score -gt $Best['Score']) { ' (best)' } + $Designates = $TestStage.GetDesignates($Stages, $SubTests, $Permutation, $TestPassed) + $TestStage.WriteDebug($null, "$([ParameterColor]'Required')$([CheckBox]::new($RequiredPassed)):$($Required.ToString($Designates)) $([ParameterColor]""Score:$Score $Better"")") + if ($TestsLeft -and $NodesLeft) { + $Designates = $TestStage.ListDesignates($Stages, $SubTests, $Permutation, $TestPassed) -join ',' + $TestStage.WriteDebug($null, "$([ParameterColor]'Optional')$([CheckBox]::new($OptionalPassed)):$Designates") } } + $TestStage.Passed = $RequiredPassed -and $OptionalPassed + # $TestStage.Passed = $RequiredPassed -and $OptionalPassed + if ($TestStage.Passed) { break } + if (-not $Best -or $Score -gt $Best['Score']) { # Capture the highest score with the least amount of issues + if (-not $Best) { $Best = @{} } + $Best['Score'] = $Score + $Best['TestPassed'] = [Dictionary[Int, Bool]]::new($TestPassed) + $Best['Permutation'] = $foreach.Copy() + } } - -#EndRegion Required nodes - -#Region Optional nodes - - if (-not $Violates) { - - foreach ($TestNode in $TestNodes) { - if ($MatchedNames.Count -ge $ChildNodes.Count) { break } - if ($AssertResults.Contains($TestNode.Name)) { continue } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $TestNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = -not $AllowExtraNodes - MatchedNames = $MatchedNames + # if ([TestStage]::Debug) { + # $ScoreFail = [ParameterColor]"(Score:$($Best['Score']))" + # $Designates = $TestStage.GetDesignates($Stages, $SubTests, $Best['Permutation'], $Best['TestPassed']) + # $TestStage.WriteDebug($null, "Selected$([CheckBox]::new($RequiredPassed)):$($Required.ToString($Designates)) $ScoreFail") + # } + if ($TestStage.Elaborate) { + for ($NodeIndex = 0; $NodeIndex -lt $SubNodes.Count; $NodeIndex++) { + if (-not $Stages[$NodeIndex]) { continue } + foreach ($Stage in $Stages[$NodeIndex].get_Values()) { + $TestStage.AddResults($Stage.Results) } - MatchNode @MatchParams - if ($AllowExtraNodes -and $MatchedNames.Count -eq $MatchCount0) { - $Violates = "When extra nodes are allowed, the node $ObjectNode should be accepted" - break - } - $AssertResults[$TestNode.Name] = $MatchedNames.Count -gt $MatchCount0 } - - if (-not $AllowExtraNodes -and $MatchedNames.Count -lt $ChildNodes.Count) { - $Count = 0; $LastName = $Null - $Names = foreach ($Name in $ChildNodes.Name) { - if ($MatchedNames.Contains($Name)) { continue } - if ($Count++ -lt 4) { - if ($ObjectNode -is [PSListNode]) { [CommandColor]$Name } - else { [StringColor][PSKeyExpression]::new($Name, [PSSerialize]::MaxKeyLength)} + } + elseif ($TestStage.Passed) { return } + elseif ($Best) { + $Permutation = $Best['Permutation'] + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + $Indices = $Permutation[$TestIndex] + if ($Indices.Count -lt $MinimumCount[$TestIndex]) { + if ($MinimumCount[$TestIndex] -eq 1) { + $TestStage.Check($SchemaNode, "The requirement $([LogicalVariable]$SubTests[$TestIndex].Name) is missing", $false) + } + else { + $TestStage.Check($SchemaNode, "$([LogicalVariable]$SubTests[$TestIndex].Name) occurred less than $($MinimumCount[$TestIndex]) times", $false) } - else { $LastName = $Name } } - $Violates = "The following nodes are not accepted: $($Names -join ', ')" - if ($LastName) { - $LastName = if ($ObjectNode -is [PSListNode]) { [CommandColor]$LastName } - else { [StringColor][PSKeyExpression]::new($LastName, [PSSerialize]::MaxKeyLength) } - $Violates += " .. $LastName" + if ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $TestStage.Check($SchemaNode, "$([LogicalVariable]$SubTests[$TestIndex].Name) occurred more than $($MaximumCount[$TestIndex]) times", $false) + } + foreach ($NodeIndex in $Indices) { + if ($null -eq $Stages[$NodeIndex] -or $null -eq $Stages[$NodeIndex][$TestIndex]) { continue } + $Results = $Stages[$NodeIndex][$TestIndex].Results + if (-not $Results -or $Results.Count -eq 0) { continue } + $TestStage.AddResults($Results) + $TestStage.FailCount += $Results.Count } } } - -#EndRegion Optional nodes - - if ($Violates -or $Elaborate) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = if ($Violates) { $Violates } else { 'All the child nodes are valid'} - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { $RefInvalidNode.Value = $Output } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } + if (-not $TestStage.Passed -and $FailCount -eq $TestStage.FailCount) { + # Presumably concerns a negative requirement + # $Not = if ($TestStage.Passed) { ' not' } + $TestStage.Check($SchemaNode, "$ObjectNode is not accepted", $TestStage.Passed) + # if (-not $Best) { + # $TestStage.Check($SchemaNode, "$ObjectNode nodes did$Not pass", $TestStage.Passed) + # } + # elseif ($Best['TestPassed'].Count) { + # foreach ($TestIndex in $Best['TestPassed'].get_Keys()) { + # if ($Best['TestPassed'][$TestIndex]) { continue } + # $TestStage.Check($SchemaNode, "The requirement $([LogicalVariable]$SubTests[$TestIndex].Name) has$Not met", $false) + # } + # } + # else { + # for ($NodeIndex = 0; $NodeIndex -lt $SubNodes.Count; $NodeIndex++) { + # if ($UsedNodes.Contains($NodeIndex)) { continue } + # $TestStage.Check($SchemaNode, "$($SubNodes[$NodeIndex]) is$Not accepted", $false) + # } + # } } + #EndRegion SubTests } } process { $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Script:UniqueCollections = @{} - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestNode @TestParams - if ($ValidateOnly) { -not $Invalid } + $TestStage = [TestStage]::new($ObjectNode, $Elaborate, (-not $ValidateOnly), 0) + TestNode $TestStage $SchemaNode + if ($ValidateOnly) { $TestStage.Passed } } } @@ -4565,22 +4900,22 @@ process { #Region Alias -Set-Alias -Name 'ConvertFrom-Expression' -Value 'cfe' -Set-Alias -Name 'Copy-ObjectGraph' -Value 'Copy-Object' -Set-Alias -Name 'Copy-ObjectGraph' -Value 'cpo' -Set-Alias -Name 'ConvertTo-Expression' -Value 'cto' -Set-Alias -Name 'Export-ObjectGraph' -Value 'epo' -Set-Alias -Name 'Export-ObjectGraph' -Value 'Export-Object' -Set-Alias -Name 'Get-ChildNode' -Value 'gcn' -Set-Alias -Name 'Get-Node' -Value 'gn' -Set-Alias -Name 'Import-ObjectGraph' -Value 'imo' -Set-Alias -Name 'Import-ObjectGraph' -Value 'Import-Object' -Set-Alias -Name 'Merge-ObjectGraph' -Value 'Merge-Object' -Set-Alias -Name 'Merge-ObjectGraph' -Value 'mgo' -Set-Alias -Name 'Get-SortObjectGraph' -Value 'Sort-ObjectGraph' -Set-Alias -Name 'Get-SortObjectGraph' -Value 'sro' -Set-Alias -Name 'Test-ObjectGraph' -Value 'Test-Object' -Set-Alias -Name 'Test-ObjectGraph' -Value 'tso' +Set-Alias -Name 'cfe' -Value 'ConvertFrom-Expression' +Set-Alias -Name 'Copy-Object' -Value 'Copy-ObjectGraph' +Set-Alias -Name 'cpo' -Value 'Copy-ObjectGraph' +Set-Alias -Name 'cto' -Value 'ConvertTo-Expression' +Set-Alias -Name 'epo' -Value 'Export-ObjectGraph' +Set-Alias -Name 'Export-Object' -Value 'Export-ObjectGraph' +Set-Alias -Name 'gcn' -Value 'Get-ChildNode' +Set-Alias -Name 'gn' -Value 'Get-Node' +Set-Alias -Name 'imo' -Value 'Import-ObjectGraph' +Set-Alias -Name 'Import-Object' -Value 'Import-ObjectGraph' +Set-Alias -Name 'Merge-Object' -Value 'Merge-ObjectGraph' +Set-Alias -Name 'mgo' -Value 'Merge-ObjectGraph' +Set-Alias -Name 'Sort-ObjectGraph' -Value 'Get-SortObjectGraph' +Set-Alias -Name 'sro' -Value 'Get-SortObjectGraph' +Set-Alias -Name 'Test-Object' -Value 'Test-ObjectGraph' +Set-Alias -Name 'tso' -Value 'Test-ObjectGraph' #EndRegion Alias diff --git a/Source/Classes/LogicalFormula.ps1 b/Source/Classes/LogicalFormula.ps1 index 79ac011..0f20402 100644 --- a/Source/Classes/LogicalFormula.ps1 +++ b/Source/Classes/LogicalFormula.ps1 @@ -94,38 +94,154 @@ class LogicalFormula : LogicalTerm { } $Start = $this.Pointer + 1 } - # elseif ($Char -le ' ' -or $Null -eq $Char) { # A space or any control code - # if ($Start -lt $this.Pointer) { - # $this.Terms.Add($this.GetUnquotedTerm($Expression, $Start, ($this.Pointer - $Start))) - # } - # $Start = $this.Pointer + 1 - # } $this.Pointer++ } if ($InString) { Throw "Missing the terminator: $InString in logical expression: $Expression" } if ($SubExpression) { Throw "Missing closing ')' in logical expression: $Expression" } } + LogicalFormula() {} LogicalFormula ([String]$Expression) { $this.GetFormula($Expression, 0) if ($this.Pointer -lt $Expression.Length) { Throw "Unexpected token ')' at position $($this.Pointer) in logical expression: $Expression" } } - LogicalFormula ([String]$Expression, $Start) { $this.GetFormula($Expression, $Start) } - Append ([LogicalOperator]$Operator, [LogicalFormula]$Formula) { - if ($Operator.Value -eq 'Not') { $this.Terms.Add([LogicalOperator]'And') } - $this.Terms.Add($Operator) - $this.Terms.AddRange($Formula.Terms) + Append ([LogicalOperator]$Operator, $Formula) { + if ($Formula -is [LogicalFormula]) { + if ($Formula.Terms.Count -eq 0) { return } + if($this.Terms.Count -eq 0) { + $This.Terms = $Formula.Terms + return + } + if ($Operator.Value -eq 'Not') { $this.Terms.Add([LogicalOperator]'And') } + $this.Terms.Add($Operator) + if ($Formula.Terms.Count -gt 1) { $this.Terms.Add($Formula) } + else { $this.Terms.Add($Formula.Terms[0]) } + } + elseif ($null -ne $Formula) { + if ($this.Terms.Count -gt 0) { $this.Terms.Add([LogicalOperator]'And') } + $this.Terms.Add([LogicalVariable]::new($Formula)) + } } + And($Formula) { $this.Append('And', $Formula) } + Or($Formula) { $this.Append('Or', $Formula) } + Xor($Formula) { $this.Append('Xor', $Formula) } - [String] ToString() { + [Object]Find([ScriptBlock]$Predicate, [Bool]$All) { + $Stack = [Stack]::new() + $Enumerator = $this.Terms.GetEnumerator() + $Term = $null + return $( + while ($true) { + while ($Enumerator.MoveNext()) { + $Term = $Enumerator.Current + if ($Term -is [LogicalFormula]) { + $Stack.Push($Enumerator) + $Enumerator = $Term.Terms.GetEnumerator() + $Term = $null + continue + } + else { + if (& $Predicate $Term) { if ($All) { $Term } else { return $Term } } + } + } + if (-not $Stack.Count) { break } + $Enumerator = $Stack.Pop() + } + ) + } + + [bool]Evaluate($Variables) { + if (-not $this.Terms) { return $null -eq $Variables -or -not $Variables.Count } + $Enumerator = $this.Terms.GetEnumerator() + $Stack = [Stack]::new() + $Stack.Push(@{ + Enumerator = $Enumerator + Accumulator = $null + Operator = $null + Negate = $null + }) + $Term, $Negate, $Operand, $Operator, $Accumulator = $null + $Score = 0 + while ($Stack.Count -gt 0) { + # Accumulator = Accumulator Operand + # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} + $Pop = $Stack.Pop() + $Enumerator = $Pop.Enumerator + $Operator = $Pop.Operator + if ($null -eq $Operator) { $Operand = $Pop.Accumulator } + else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } + $Negate = $Pop.Negate + $Compute = $null -notin $Operand, $Operator, $Accumulator + while ($Compute -or $Enumerator.MoveNext()) { + if ($Compute) { $Compute = $false } + else { + $Term = $Enumerator.Current + if ($Term -is [LogicalVariable]) { + if ($Variables -is [ScriptBlock]) { $Operand = [Bool]$Term.Value.foreach($Variables) } + elseif ($Variables -is [IDictionary]) { $Operand = [Bool]$Variables[$Term.Value] } + elseif ($Variables -is [IEnumerable]) { $Operand = $Variables.Contains($Term.Value) } + else { $Operand = $Variables -eq $Term.Value } + } + elseif ($Term -is [LogicalOperator]) { + if ($Term.Value -eq 'Not') { $Negate = -not $Negate } + elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } + else { throw [InvalidOperationException]"Unknown logical operator: $Term" } + } + elseif ($Term -is [LogicalFormula]) { + $Stack.Push(@{ + Enumerator = $Enumerator + Accumulator = $Accumulator + Operator = $Operator + Negate = $Negate + }) + $Accumulator, $Operator, $Negate = $null + $Enumerator = $Term.Terms.GetEnumerator() + continue + } + else { throw [InvalidOperationException]"Unknown logical term: $Term" } + } + if ($null -ne $Operand) { + $Score++ + if ($null -eq $Accumulator -xor $null -eq $Operator) { + if ($Accumulator) { throw [InvalidOperationException]"Missing operator before: $Term" } + else { throw [InvalidOperationException]"Missing variable before: $Operator $Term" } + } + $Operand = $Operand -xor $Negate + $Negate = $null + if ($Operator -eq 'And') { + $Operator = $null + if ($Accumulator -eq $false) { break } + $Accumulator = $Accumulator -and $Operand + } + elseif ($Operator -eq 'Or') { + $Operator = $null + if ($Accumulator -eq $true) { break } + $Accumulator = $Accumulator -or $Operand + } + elseif ($Operator -eq 'Xor') { + $Operator = $null + $Accumulator = $Accumulator -xor $Operand + } + else { $Accumulator = $Operand } + $Operand = $Null + } + } + if ($null -ne $Operator -or $null -ne $Negate) { throw [InvalidOperationException]"Missing variable after $Operator" } + } + if ($null -eq $Accumulator) { throw "The accumulator isn't defined" } + return $Accumulator + } + + [String] ToString() { return $this.ToString($null) } + [String] ToString([IDictionary]$Extents) { $StringBuilder = [System.Text.StringBuilder]::new() - $Stack = [System.Collections.Stack]::new() + $Stack = [Stack]::new() $Enumerator = $this.Terms.GetEnumerator() $Term = $null while ($true) { @@ -136,10 +252,18 @@ class LogicalFormula : LogicalTerm { } $Term = $Enumerator.Current if ($Term -is [LogicalVariable]) { - if ($Term.Value -is [String]) { $null = $StringBuilder.Append([ANSI]::VariableColor) } - else { $null = $StringBuilder.Append([ANSI]::NumberColor) } + if ($Term.Value -is [String]) { $null = $StringBuilder.Append([ANSI]::VariableColor) } + else { $null = $StringBuilder.Append([ANSI]::NumberColor) } + $null = $StringBuilder.Append($Term) + if ($Extents) { + $null = $StringBuilder.Append([ANSI]::EmphasisColor) + $null = $StringBuilder.Append($Extents[$Term.Value]) + } + } + elseif ($Term -is [LogicalOperator]) { + $null = $StringBuilder.Append([ANSI]::OperatorColor) + $null = $StringBuilder.Append($Term) } - elseif ($Term -is [LogicalOperator]) { $null = $StringBuilder.Append([ANSI]::OperatorColor) } else { # if ($Term -is [LogicalFormula]) $null = $StringBuilder.Append([ANSI]::StringColor) $null = $StringBuilder.Append('(') @@ -148,7 +272,6 @@ class LogicalFormula : LogicalTerm { $Term = $null continue } - $null = $StringBuilder.Append($Term) } if (-not $Stack.Count) { $null = $StringBuilder.Append([ANSI]::ResetColor) diff --git a/Source/Classes/NodeParser.ps1 b/Source/Classes/NodeParser.ps1 index daea417..dfcf189 100644 --- a/Source/Classes/NodeParser.ps1 +++ b/Source/Classes/NodeParser.ps1 @@ -153,7 +153,7 @@ Class PSNode : IComparable { return $Node } - static [PSNode] ParseInput($Object) { return [PSNode]::parseInput($Object, 0) } + static [PSNode] ParseInput($Object) { return [PSNode]::ParseInput($Object, 0) } static [int] Compare($Left, $Right) { return [ObjectComparer]::new().Compare($Left, $Right) @@ -251,67 +251,82 @@ Class PSNode : IComparable { } hidden CollectNodes($NodeTable, [XdnPath]$Path, [Int]$PathIndex) { + if ($PathIndex -ge $Path.Entries.Count) { + $NodeTable[$this.getPathName()] = $this + return + } $Entry = $Path.Entries[$PathIndex] - $NextIndex = if ($PathIndex -lt $Path.Entries.Count -1) { $PathIndex + 1 } - $NextEntry = if ($NextIndex) { $Path.Entries[$NextIndex] } - $Equals = if ($NextEntry -and $NextEntry.Key -eq 'Equals') { - $NextEntry.Value - $NextIndex = if ($NextIndex -lt $Path.Entries.Count -1) { $NextIndex + 1 } - } - switch ($Entry.Key) { - Root { - $Node = $this.RootNode - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } + if ($Entry.Key -eq 'Root') { + $this.RootNode.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + } + elseif ($Entry.Key -eq 'Ancestor') { + $Node = $this + for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode } + if ($i -eq 0) { $Node.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } + } + elseif ($Entry.Key -eq 'Index') { + if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) { + $this.GetChildNode([Int]$Entry.Value).CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } - Ancestor { - $Node = $this - for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode } - if ($i -eq 0) { # else: reached root boundary - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } + } + elseif ($Entry.Key -eq 'Equals') { + if ($this -is [PSLeafNode]) { + foreach ($Value in $Entry.Value) { + if ($this._Value -like $Value) { + $this.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + break + } } } - Index { - if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) { - $Node = $this.GetChildNode([Int]$Entry.Value) - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } + } + elseif ($this -is [PSListNode]) { # Member access enumeration + foreach ($Node in $this.get_ChildNodes()) { + $Node.CollectNodes($NodeTable, $Path, $PathIndex) } - Default { # Child, Descendant - if ($this -is [PSListNode]) { # Member access enumeration - foreach ($Node in $this.get_ChildNodes()) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) + } + elseif ($this -is [PSMapNode]) { + $Count0 = $NodeTable.get_Count() + foreach ($Value in $Entry.Value) { + $Name = $Value._Value + if ($Value.ContainsWildcard()) { + $CaseMatters = $this.CaseMatters + foreach ($Node in $this.ChildNodes) { + if ($CaseMatters) { if ($Node.Name -cnotlike $Name) { continue } } + else { if ($Node.Name -notlike $Name) { continue } } + $Node.CollectNodes($NodeTable, $Path, ($PathIndex + 1)) } } - elseif ($this -is [PSMapNode]) { - $Found = $False - $ChildNodes = $this.get_ChildNodes() - foreach ($Node in $ChildNodes) { - if ($Entry.Value -eq $Node.Name -and (-not $Equals -or ($Node -is [PSLeafNode] -and $Equals -eq $Node._Value))) { - $Found = $True - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - } - if (-not $Found -and $Entry.Key -eq 'Descendant') { - foreach ($Node in $ChildNodes) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) - } - } + elseif ($this.Contains($Name)) { + $this.GetChildNode($Name).CollectNodes($NodeTable, $Path, ($PathIndex + 1)) + } + } + if ( + ($Entry.Key -eq 'Offspring') -or + ($Entry.Key -eq 'Descendant' -and $NodeTable.get_Count() -eq $Count0) + ) { + foreach ($Node in $this.get_ChildNodes()) { + $Node.CollectNodes($NodeTable, $Path, $PathIndex) } } } } - [Object] GetNode([XdnPath]$Path) { + + [Object]GetNode([XdnPath]$Path) { $NodeTable = [system.collections.generic.dictionary[String, PSNode]]::new() # Case sensitive (case insensitive map nodes use the same name) $this.CollectNodes($NodeTable, $Path, 0) if ($NodeTable.Count -eq 0) { return @() } if ($NodeTable.Count -eq 1) { return $NodeTable[$NodeTable.Keys] } else { return [PSNode[]]$NodeTable.Values } } + + [string]ToString() { + return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" + } + + hidden [string]get_DisplayValue() { + return "$([TypeColor][PSSerialize]::new($this._Value, [PSLanguageMode]'NoLanguage'))" + } } Class PSLeafNode : PSNode { @@ -330,10 +345,6 @@ Class PSLeafNode : PSNode { else { return $this._Value.GetHashCode() } } } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } } Class PSCollectionNode : PSNode { @@ -570,10 +581,6 @@ Class PSListNode : PSCollectionNode { } return $this._HashCode[$CaseSensitive] } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } } Class PSMapNode : PSCollectionNode { @@ -697,8 +704,8 @@ Class PSDictionaryNode : PSMapNode { if ($this.get_CaseMatters()) { $ChildNode = $this.Cache['ChildNode'] $this.Cache['ChildNode'] = [HashTable]::new() # Create a new cache as it appears to be case sensitive - foreach ($Key in $ChildNode.get_Keys()) { # Migrate the content - $this.Cache.ChildNode[$Key] = $ChildNode[$Key] + foreach ($Name in $ChildNode.get_Keys()) { # Migrate the content + $this.Cache.ChildNode[$Name] = $ChildNode[$Name] } } } @@ -724,10 +731,6 @@ Class PSDictionaryNode : PSMapNode { } return $this.Cache['ChildNodes'] } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } } Class PSObjectNode : PSMapNode { @@ -778,7 +781,7 @@ Class PSObjectNode : PSMapNode { $this._Value.PSObject.Properties[$Name].Value = $Value } else { - Add-Member -InputObject $this._Value -Type NoteProperty -Name $Name -Value $Value + $this._Value.PSObject.Properties.Add([PSNoteProperty]::new($Name, $Value)) $this.Cache.Remove('ChildNodes') } } @@ -820,14 +823,8 @@ Class PSObjectNode : PSMapNode { hidden [Object[]]get_ChildNodes() { if (-not $this.Cache.ContainsKey('ChildNodes')) { $ChildNodes = foreach ($Property in $this._Value.PSObject.Properties) { $this.GetChildNode($Property.Name) } - # if ($Property.Value -isnot [Reflection.MemberInfo]) { $this.GetChildNode($Property.Name) } - # } if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } } return $this.Cache['ChildNodes'] } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } } diff --git a/Source/Classes/ObjectComparer.ps1 b/Source/Classes/ObjectComparer.ps1 index cf64780..7e4bcea 100644 --- a/Source/Classes/ObjectComparer.ps1 +++ b/Source/Classes/ObjectComparer.ps1 @@ -177,7 +177,7 @@ class ObjectComparer { Path = $Node2.Path + "[$Index2]" $this.Issue = 'Exists' $this.Name1 = $Null - $this.Name2 = if ($Item2 -is [PSLeafNode]) { "$($Item2.Value)" } else { "[$($Item2.ValueType)]" } + $this.Name2 = $Item2 }) } } @@ -190,7 +190,7 @@ class ObjectComparer { $this.Differences.Add([PSCustomObject]@{ Path = $Node1.Path + "[$Index1]" $this.Issue = 'Exists' - $this.Name1 = if ($Item1 -is [PSLeafNode]) { "$($Item1.Value)" } else { "[$($Item1.ValueType)]" } + $this.Name1 = $Item1 $this.Name2 = $Null }) } diff --git a/Source/Classes/PSSerialize.ps1 b/Source/Classes/PSSerialize.ps1 index dea471d..fdcb69a 100644 --- a/Source/Classes/PSSerialize.ps1 +++ b/Source/Classes/PSSerialize.ps1 @@ -249,7 +249,10 @@ Class PSSerialize { $this.StringBuilder.Append(',') $this.NewWord() } - elseif ($ExpandSingle) { $this.NewWord('') } + else { + if ($ExpandSingle) { $this.NewWord('') } + if ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -is [PSListNode]) { $this.StringBuilder.Append(',') } + } $this.Stringify($ChildNode) } $this.Offset-- @@ -274,7 +277,11 @@ Class PSSerialize { $this.StringBuilder.Append([VariableColor]( [PSKeyExpression]::new($ChildNodes[$Index].Name, [PSSerialize]::MaxKeyLength))) $this.StringBuilder.Append('=') - if (-not $IsSubNode -or $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength) { + if ( + -not $IsSubNode -or + $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength -or + ($ChildNodes.Count -eq 1 -and $ChildNodes[$Index] -is [PSLeafNode]) + ) { $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) } else { $this.StringBuilder.Append([Abbreviate]::Ellipses) } diff --git a/Source/Classes/PSStyleTypes.ps1 b/Source/Classes/PSStyleTypes.ps1 index 986a579..7ca8dca 100644 --- a/Source/Classes/PSStyleTypes.ps1 +++ b/Source/Classes/PSStyleTypes.ps1 @@ -25,6 +25,17 @@ Class ANSI { static [String]$InverseOff Static ANSI() { + # https://stackoverflow.com/questions/38045245/how-to-call-getstdhandle-getconsolemode-from-powershell + $MethodDefinitions = @' +[DllImport("kernel32.dll", SetLastError = true)] +public static extern IntPtr GetStdHandle(int nStdHandle); +[DllImport("kernel32.dll", SetLastError = true)] +public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); +'@ + $Kernel32 = Add-Type -MemberDefinition $MethodDefinitions -Name 'Kernel32' -Namespace 'Win32' -PassThru + $hConsoleHandle = $Kernel32::GetStdHandle(-11) # STD_OUTPUT_HANDLE + if (-not $Kernel32::GetConsoleMode($hConsoleHandle, [ref]0)) { return } + $PSReadLineOption = try { Get-PSReadLineOption -ErrorAction SilentlyContinue } catch { $null } if (-not $PSReadLineOption) { return } $ANSIType = [ANSI] -as [Type] diff --git a/Source/Cmdlets/Compare-ObjectGraph.ps1 b/Source/Cmdlets/Compare-ObjectGraph.ps1 index 9614fbd..9fa5eef 100644 --- a/Source/Cmdlets/Compare-ObjectGraph.ps1 +++ b/Source/Cmdlets/Compare-ObjectGraph.ps1 @@ -1,67 +1,65 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Compare Object Graph +Compare Object Graph .DESCRIPTION - Deep compares two Object Graph and lists the differences between them. +Deep compares two Object Graph and lists the differences between them. .PARAMETER InputObject - The input object that will be compared with the reference object (see: [-Reference] parameter). +The input object that will be compared with the reference object (see: [-Reference] parameter). - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: +> [!NOTE] +> Multiple input object might be provided via the pipeline. +> The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. +> To avoid a list of (root) objects to unroll, use the **comma operator**: - ,$InputObject | Compare-ObjectGraph $Reference. + ,$InputObject | Compare-ObjectGraph $Reference. .PARAMETER Reference - The reference that is used to compared with the input object (see: [-InputObject] parameter). +The reference that is used to compared with the input object (see: [-InputObject] parameter). .PARAMETER PrimaryKey - If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched - based on the values of the `-PrimaryKey` supplied. +If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched +based on the values of the `-PrimaryKey` supplied. .PARAMETER IsEqual - If set, the cmdlet will return a boolean (`$true` or `$false`). - As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. +If set, the cmdlet will return a boolean (`$true` or `$false`). +As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. .PARAMETER MatchCase - Unless the `-MatchCase` switch is provided, string values are considered case insensitive. +Unless the `-MatchCase` switch is provided, string values are considered case insensitive. - > [!NOTE] - > Dictionary keys are compared based on the `$Reference`. - > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison - > is case insensitive otherwise the comparer supplied with the dictionary is used. +> [!NOTE] +> Dictionary keys are compared based on the `$Reference`. +> if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison +> is case insensitive otherwise the comparer supplied with the dictionary is used. .PARAMETER MatchType - Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the - `$Reference` object is leading. Meaning `$Reference -eq $InputObject`: +Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the +`$Reference` object is leading. Meaning `$Reference -eq $InputObject`: - '1.0' -eq 1.0 # $false - 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) + '1.0' -eq 1.0 # $false + 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) .PARAMETER IgnoreLisOrder - By default, items in a list are matched independent of the order (meaning by index position). - If the `-IgnoreListOrder` switch is supplied, any list in the `$InputObject` is searched for a match - with the reference. +By default, items in a list are matched independent of the order (meaning by index position). +If the `-IgnoreListOrder` switch is supplied, any list in the `$InputObject` is searched for a match +with the reference. - > [!NOTE] - > Regardless the list order, any dictionary lists are matched by the primary key (if supplied) first. +> [!NOTE] +> Regardless the list order, any dictionary lists are matched by the primary key (if supplied) first. .PARAMETER MatchMapOrder - By default, items in dictionary (including properties of an PSCustomObject or Component Object) are - matched by their key name (independent of the order). - If the `-MatchMapOrder` switch is supplied, each entry is also validated by the position. +By default, items in dictionary (including properties of an PSCustomObject or Component Object) are +matched by their key name (independent of the order). +If the `-MatchMapOrder` switch is supplied, each entry is also validated by the position. - > [!NOTE] - > A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, - the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. +> [!NOTE] +> A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, +the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. .PARAMETER MaxDepth - The maximal depth to recursively compare each embedded property (default: 10). +The maximal depth to recursively compare each embedded property (default: 10). #> [CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Compare-ObjectGraph.md')] param( diff --git a/Source/Cmdlets/ConvertFrom-Expression.ps1 b/Source/Cmdlets/ConvertFrom-Expression.ps1 index 53164b1..6c4fc03 100644 --- a/Source/Cmdlets/ConvertFrom-Expression.ps1 +++ b/Source/Cmdlets/ConvertFrom-Expression.ps1 @@ -1,44 +1,42 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Deserializes a PowerShell expression to an object. +Deserializes a PowerShell expression to an object. .DESCRIPTION - The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph - existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. +The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph +existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. .PARAMETER InputObject - Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, - or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. +Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, +or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. - The **InputObject** parameter is required, but its value can be an empty string. - The **InputObject** value can't be `$null` or an empty string. +The **InputObject** parameter is required, but its value can be an empty string. +The **InputObject** value can't be `$null` or an empty string. .PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] - - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. +Defines which object types are allowed for the deserialization, see: [About language modes][2] + +* Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + `[String]`, `[Array]` or `[HashTable]`. +* Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. + +> [!Caution] +> +> In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, +> CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. +> +> Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. +> Verify that the class types in the expression are safe before instantiating them. In general, it is +> best to design your configuration expressions with restricted or constrained classes, rather than +> allowing full freeform expressions. .PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given list type. +If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown +or denied type initializer will be converted to the given list type. .PARAMETER MapAs - If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given map (dictionary or object) type. +If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown +or denied type initializer will be converted to the given map (dictionary or object) type. #> diff --git a/Source/Cmdlets/ConvertTo-Expression.ps1 b/Source/Cmdlets/ConvertTo-Expression.ps1 index 0b75b07..d036a6d 100644 --- a/Source/Cmdlets/ConvertTo-Expression.ps1 +++ b/Source/Cmdlets/ConvertTo-Expression.ps1 @@ -1,113 +1,111 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Serializes an object to a PowerShell expression. +Serializes an object to a PowerShell expression. .DESCRIPTION - The ConvertTo-Expression cmdlet converts (serializes) an object to a PowerShell expression. - The object can be stored in a variable, (.psd1) file or any other common storage for later use or to be ported - to another system. +The ConvertTo-Expression cmdlet converts (serializes) an object to a PowerShell expression. +The object can be stored in a variable, (.psd1) file or any other common storage for later use or to be ported +to another system. - expressions might be restored to an object using the native [Invoke-Expression] cmdlet: +expressions might be restored to an object using the native [Invoke-Expression] cmdlet: - $Object = Invoke-Expression ($Object | ConvertTo-Expression) + $Object = Invoke-Expression ($Object | ConvertTo-Expression) - > [!Warning] - > Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using `Invoke-Expression` - > to run a command that the user enters, verify that the command is safe to run before running it. - > In general, it is best to restore your objects using [ConvertFrom-Expression]. +> [!Warning] +> Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using `Invoke-Expression` +> to run a command that the user enters, verify that the command is safe to run before running it. +> In general, it is best to restore your objects using [ConvertFrom-Expression]. - > [!Note] - > Some object types can not be reconstructed from a simple serialized expression. +> [!Note] +> Some object types can not be reconstructed from a simple serialized expression. .INPUTS - Any. Each objects provided through the pipeline will converted to an expression. To concatenate all piped - objects in a single expression, use the unary comma operator, e.g.: `,$Object | ConvertTo-Expression` +Any. Each objects provided through the pipeline will converted to an expression. To concatenate all piped +objects in a single expression, use the unary comma operator, e.g.: `,$Object | ConvertTo-Expression` .OUTPUTS - String[]. `ConvertTo-Expression` returns a PowerShell [String] expression for each input object. +String[]. `ConvertTo-Expression` returns a PowerShell [String] expression for each input object. .PARAMETER InputObject - Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, - or type a command or expression that gets the objects. You can also pipe one or more objects to - `ConvertTo-Expression.` +Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, +or type a command or expression that gets the objects. You can also pipe one or more objects to +`ConvertTo-Expression.` .PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: +Defines which object types are allowed for the serialization, see: [About language modes][2] +If a specific type isn't allowed in the given language mode, it will be substituted by: - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) +* **`$Null`** in case of a null value +* **`$False`** in case of a boolean false +* **`$True`** in case of a boolean true +* **A number** in case of a primitive value +* **A string** in case of a string or any other **leaf** node +* `@(...)` for an array (**list** node) +* `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - See the [PSNode Object Parser][1] for a detailed definition on node types. +See the [PSNode Object Parser][1] for a detailed definition on node types. .PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. +Defines up till what level the collections will be expanded in the output. - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. +* A `-ExpandDepth 0` will create a single line expression. +* A `-ExpandDepth -1` will compress the single line by removing command spaces. - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. +> [!Note] +> White spaces (as newline characters and spaces) will not be removed from the content +> of a (here) string. .PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) +By default, restricted language types initializers are suppressed. +When the `Explicit` switch is set, *all* values will be prefixed with an initializer +(as e.g. `[Long]` and `[Array]`) - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode +> [!Note] +> The `-Explicit` switch can not be used in **restricted** language mode .PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. +In case a value is prefixed with an initializer, the full type name of the initializer is used. - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). +> [!Note] +> The `-FullTypename` switch can not be used in **restricted** language mode and will only be +> meaningful if the initializer is used (see also the [-Explicit] switch). .PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. +If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - By default the fidelity of an object expression will end if: +By default the fidelity of an object expression will end if: - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. +1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) +2) the (embedded) object expression is able to round trip. - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. +An object is able to roundtrip if the resulted expression of the object itself or one of +its properties (prefixed with the type initializer) can be used to rebuild the object. - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). +The advantage of the default fidelity is that the resulted expression round trips (aka the +object might be rebuild from the expression), the disadvantage is that information hold by +less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. +The advantage of the high fidelity switch is that all the information of the underlying +properties is shown, yet any constrained or full object type will likely fail to rebuild +due to constructor limitations such as readonly property. - > [!Note] - > The Object property `TypeId = []` is always excluded. +> [!Note] +> The Object property `TypeId = []` is always excluded. .PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. +(List or map) collections nodes that contain a single item will not be expanded unless this +`-ExpandSingleton` is supplied. .PARAMETER IndentSize - Specifies indent used for the nested properties. +Specifies indent used for the nested properties. .PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). +Specifies how many levels of contained objects are included in the PowerShell representation. +The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('cto')] diff --git a/Source/Cmdlets/Copy-ObjectGraph.ps1 b/Source/Cmdlets/Copy-ObjectGraph.ps1 index 02a7cb6..c65dc4d 100644 --- a/Source/Cmdlets/Copy-ObjectGraph.ps1 +++ b/Source/Cmdlets/Copy-ObjectGraph.ps1 @@ -1,46 +1,44 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Copy object graph +Copy object graph .DESCRIPTION - Recursively ("deep") copies a object graph. +Recursively ("deep") copies a object graph. .EXAMPLE - # Deep copy a complete object graph into a new object graph +# Deep copy a complete object graph into a new object graph - $NewObjectGraph = Copy-ObjectGraph $ObjectGraph + $NewObjectGraph = Copy-ObjectGraph $ObjectGraph .EXAMPLE - # Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects +# Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects - $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject + $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject .EXAMPLE - # Convert a Json string to an object graph with (case insensitive) ordered dictionaries +# Convert a Json string to an object graph with (case insensitive) ordered dictionaries - $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered]@{}) + $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered]@{}) .PARAMETER InputObject - The input object that will be recursively copied. +The input object that will be recursively copied. .PARAMETER ListAs - If supplied, lists will be converted to the given type (or type of the supplied object example). +If supplied, lists will be converted to the given type (or type of the supplied object example). .PARAMETER DictionaryAs - If supplied, dictionaries will be converted to the given type (or type of the supplied object example). - This parameter also accepts the [`PSCustomObject`][1] types - By default (if the [-DictionaryAs] parameters is omitted), - [`Component`][2] objects will be converted to a [`PSCustomObject`][1] type. +If supplied, dictionaries will be converted to the given type (or type of the supplied object example). +This parameter also accepts the [`PSCustomObject`][1] types +By default (if the [-DictionaryAs] parameters is omitted), +[`Component`][2] objects will be converted to a [`PSCustomObject`][1] type. .PARAMETER ExcludeLeafs - If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. - If omitted, each leaf will be shallow copied +If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. +If omitted, each leaf will be shallow copied .LINK - [1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" - [2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" +[1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" +[2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" #> [Alias('Copy-Object', 'cpo')] [OutputType([Object[]])] diff --git a/Source/Cmdlets/Export-ObjectGraph.ps1 b/Source/Cmdlets/Export-ObjectGraph.ps1 index a926cfc..2b58073 100644 --- a/Source/Cmdlets/Export-ObjectGraph.ps1 +++ b/Source/Cmdlets/Export-ObjectGraph.ps1 @@ -1,101 +1,99 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Serializes a PowerShell File or object-graph and exports it to a PowerShell (data) file. +Serializes a PowerShell File or object-graph and exports it to a PowerShell (data) file. .DESCRIPTION - The `Export-ObjectGraph` cmdlet converts a PowerShell (complex) object to an PowerShell expression - and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file. +The `Export-ObjectGraph` cmdlet converts a PowerShell (complex) object to an PowerShell expression +and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file. .PARAMETER Path - Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. - Wildcard characters are permitted. +Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. +Wildcard characters are permitted. .PARAMETER LiteralPath - Specifies a path to one or more locations where PowerShell should export the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. +Specifies a path to one or more locations where PowerShell should export the object-graph. +The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. +If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell +PowerShell not to interpret any characters as escape sequences. .PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: +Defines which object types are allowed for the serialization, see: [About language modes][2] +If a specific type isn't allowed in the given language mode, it will be substituted by: - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) +* **`$Null`** in case of a null value +* **`$False`** in case of a boolean false +* **`$True`** in case of a boolean true +* **A number** in case of a primitive value +* **A string** in case of a string or any other **leaf** node +* `@(...)` for an array (**list** node) +* `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - See the [PSNode Object Parser][1] for a detailed definition on node types. +See the [PSNode Object Parser][1] for a detailed definition on node types. .PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. +Defines up till what level the collections will be expanded in the output. - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. +* A `-ExpandDepth 0` will create a single line expression. +* A `-ExpandDepth -1` will compress the single line by removing command spaces. - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. +> [!Note] +> White spaces (as newline characters and spaces) will not be removed from the content +> of a (here) string. .PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) +By default, restricted language types initializers are suppressed. +When the `Explicit` switch is set, *all* values will be prefixed with an initializer +(as e.g. `[Long]` and `[Array]`) - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode +> [!Note] +> The `-Explicit` switch can not be used in **restricted** language mode .PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. +In case a value is prefixed with an initializer, the full type name of the initializer is used. - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). +> [!Note] +> The `-FullTypename` switch can not be used in **restricted** language mode and will only be +> meaningful if the initializer is used (see also the [-Explicit] switch). .PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. +If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - By default the fidelity of an object expression will end if: +By default the fidelity of an object expression will end if: - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. +1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) +2) the (embedded) object expression is able to round trip. - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. +An object is able to roundtrip if the resulted expression of the object itself or one of +its properties (prefixed with the type initializer) can be used to rebuild the object. - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). +The advantage of the default fidelity is that the resulted expression round trips (aka the +object might be rebuild from the expression), the disadvantage is that information hold by +less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. +The advantage of the high fidelity switch is that all the information of the underlying +properties is shown, yet any constrained or full object type will likely fail to rebuild +due to constructor limitations such as readonly property. - > [!Note] - > Objects properties of type `[Reflection.MemberInfo]` are always excluded. +> [!Note] +> Objects properties of type `[Reflection.MemberInfo]` are always excluded. .PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. +(List or map) collections nodes that contain a single item will not be expanded unless this +`-ExpandSingleton` is supplied. .PARAMETER IndentSize - Specifies indent used for the nested properties. +Specifies indent used for the nested properties. .PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). +Specifies how many levels of contained objects are included in the PowerShell representation. +The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). .PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. +Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('Export-Object', 'epo')] diff --git a/Source/Cmdlets/Get-ChildNode.ps1 b/Source/Cmdlets/Get-ChildNode.ps1 index 24acc43..f820255 100644 --- a/Source/Cmdlets/Get-ChildNode.ps1 +++ b/Source/Cmdlets/Get-ChildNode.ps1 @@ -1,164 +1,162 @@ -using module .\..\..\..\ObjectGraphTools - Using NameSpace System.Management.Automation.Language <# .SYNOPSIS - Gets the child nodes of an object-graph +Gets the child nodes of an object-graph .DESCRIPTION - Gets the (unique) nodes and child nodes in one or more specified locations of an object-graph - The returned nodes are unique even if the provide list of input parent nodes have an overlap. +Gets the (unique) nodes and child nodes in one or more specified locations of an object-graph +The returned nodes are unique even if the provide list of input parent nodes have an overlap. .EXAMPLE - # Select all leaf nodes in a object graph - - Given the following object graph: - - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } +# Select all leaf nodes in a object graph + +Given the following object graph: + + $Object = @{ + Comment = 'Sample ObjectGraph' + Data = @( + @{ + Index = 1 + Name = 'One' + Comment = 'First item' + } + @{ + Index = 2 + Name = 'Two' + Comment = 'Second item' + } + @{ + Index = 3 + Name = 'Three' + Comment = 'Third item' + } + ) + } - The following example will receive all leaf nodes: +The following example will receive all leaf nodes: - $Object | Get-ChildNode -Recurse -Leaf + $Object | Get-ChildNode -Recurse -Leaf - Path Name Depth Value - ---- ---- ----- ----- - .Data[0].Comment Comment 3 First item - .Data[0].Name Name 3 One - .Data[0].Index Index 3 1 - .Data[1].Comment Comment 3 Second item - .Data[1].Name Name 3 Two - .Data[1].Index Index 3 2 - .Data[2].Comment Comment 3 Third item - .Data[2].Name Name 3 Three - .Data[2].Index Index 3 3 - .Comment Comment 1 Sample ObjectGraph + Path Name Depth Value + ---- ---- ----- ----- + .Data[0].Comment Comment 3 First item + .Data[0].Name Name 3 One + .Data[0].Index Index 3 1 + .Data[1].Comment Comment 3 Second item + .Data[1].Name Name 3 Two + .Data[1].Index Index 3 2 + .Data[2].Comment Comment 3 Third item + .Data[2].Name Name 3 Three + .Data[2].Index Index 3 3 + .Comment Comment 1 Sample ObjectGraph .EXAMPLE - # update a property - - The following example selects all child nodes named `Comment` at a depth of `3`. - Than filters the one that has an `Index` sibling with the value `2` and eventually - sets the value (of the `Comment` node) to: 'Two to the Loo'. - - $Object | Get-ChildNode -AtDepth 3 -Include Comment | - Where-Object { $_.ParentNode.GetChildNode('Index').Value -eq 2 } | - ForEach-Object { $_.Value = 'Two to the Loo' } - - ConvertTo-Expression $Object - - @{ - Data = - @{ - Comment = 'First item' - Name = 'One' - Index = 1 - }, - @{ - Comment = 'Two to the Loo' - Name = 'Two' - Index = 2 - }, - @{ - Comment = 'Third item' - Name = 'Three' - Index = 3 - } - Comment = 'Sample ObjectGraph' - } +# update a property + +The following example selects all child nodes named `Comment` at a depth of `3`. +Than filters the one that has an `Index` sibling with the value `2` and eventually +sets the value (of the `Comment` node) to: 'Two to the Loo'. + + $Object | Get-ChildNode -AtDepth 3 -Include Comment | + Where-Object { $_.ParentNode.GetChildNode('Index').Value -eq 2 } | + ForEach-Object { $_.Value = 'Two to the Loo' } + + ConvertTo-Expression $Object + + @{ + Data = + @{ + Comment = 'First item' + Name = 'One' + Index = 1 + }, + @{ + Comment = 'Two to the Loo' + Name = 'Two' + Index = 2 + }, + @{ + Comment = 'Third item' + Name = 'Three' + Index = 3 + } + Comment = 'Sample ObjectGraph' + } - See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties and methods. +See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties and methods. .PARAMETER InputObject - The concerned object graph or node. +The concerned object graph or node. .PARAMETER Recurse - Recursively iterates through all embedded property objects (nodes) to get the selected nodes. - The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` - of the (root) node. To change the maximum depth the (root) node needs to be loaded first, e.g.: +Recursively iterates through all embedded property objects (nodes) to get the selected nodes. +The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` +of the (root) node. To change the maximum depth the (root) node needs to be loaded first, e.g.: - Get-Node -Depth 20 | Get-ChildNode ... + Get-Node -Depth 20 | Get-ChildNode ... - (See also: [`Get-Node`][2]) +(See also: [`Get-Node`][2]) - > [!NOTE] - > If the [AtDepth] parameter is supplied, the object graph is recursively searched anyways - > for the selected nodes up till the deepest given `AtDepth` value. +> [!NOTE] +> If the [AtDepth] parameter is supplied, the object graph is recursively searched anyways +> for the selected nodes up till the deepest given `AtDepth` value. .PARAMETER AtDepth - When defined, only returns nodes at the given depth(s). +When defined, only returns nodes at the given depth(s). - > [!NOTE] - > The nodes below the `MaxDepth` can not be retrieved. +> [!NOTE] +> The nodes below the `MaxDepth` can not be retrieved. .PARAMETER ListChild - Returns the closest nodes derived from a **list node**. +Returns the closest nodes derived from a **list node**. .PARAMETER Include - Returns only nodes derived from a **map node** including only the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. +Returns only nodes derived from a **map node** including only the ones specified by one or more +string patterns defined by this parameter. Wildcard characters are permitted. - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. +> [!NOTE] +> The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied +> after the inclusions, which can affect the final output. .PARAMETER Exclude - Returns only nodes derived from a **map node** excluding the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. +Returns only nodes derived from a **map node** excluding the ones specified by one or more +string patterns defined by this parameter. Wildcard characters are permitted. - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. +> [!NOTE] +> The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied +> after the inclusions, which can affect the final output. .PARAMETER Literal - The values of the [-Include] - and [-Exclude] parameters are used exactly as it is typed. - No characters are interpreted as wildcards. +The values of the [-Include] - and [-Exclude] parameters are used exactly as it is typed. +No characters are interpreted as wildcards. .PARAMETER Leaf - Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. - You can use the [-Recurse] parameter with the [-Leaf] parameter. +Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. +You can use the [-Recurse] parameter with the [-Leaf] parameter. .PARAMETER IncludeSelf - Includes the current node with the returned child nodes. +Includes the current node with the returned child nodes. .PARAMETER ValueOnly - returns the value of the node instead of the node itself. +returns the value of the node instead of the node itself. .PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: +Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. +The failsafe will prevent infinitive loops for circular references as e.g. in: - $Test = @{Guid = New-Guid} - $Test.Parent = $Test + $Test = @{Guid = New-Guid} + $Test.Parent = $Test - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. +The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. +> [!Note] +> The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node +> at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" #> [Alias('gcn')] diff --git a/Source/Cmdlets/Get-Node.ps1 b/Source/Cmdlets/Get-Node.ps1 index a85c72a..e59e121 100644 --- a/Source/Cmdlets/Get-Node.ps1 +++ b/Source/Cmdlets/Get-Node.ps1 @@ -1,116 +1,114 @@ -using module .\..\..\..\ObjectGraphTools - Using NameSpace System.Management.Automation.Language <# .SYNOPSIS - Get a node +Get a node .DESCRIPTION - The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. +The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. .EXAMPLE - # Parse a object graph to a node instance +# Parse a object graph to a node instance - The following example parses a hash table to `[PSNode]` instance: +The following example parses a hash table to `[PSNode]` instance: - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node + @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node - PathName Name Depth Value - -------- ---- ----- ----- - 0 {My, Object} + PathName Name Depth Value + -------- ---- ----- ----- + 0 {My, Object} .EXAMPLE - # select a sub node in an object graph +# select a sub node in an object graph - The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) - item in the `My` map node +The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) +item in the `My` map node - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node My[1] + @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node My[1] - PathName Name Depth Value - -------- ---- ----- ----- - My[1] 1 2 2 + PathName Name Depth Value + -------- ---- ----- ----- + My[1] 1 2 2 .EXAMPLE - # Change the price of the **PowerShell** book: +# Change the price of the **PowerShell** book: - $ObjectGraph = - @{ - BookStore = @( - @{ - Book = @{ - Title = 'Harry Potter' - Price = 29.99 - } - }, - @{ - Book = @{ - Title = 'Learning PowerShell' - Price = 39.95 - } - } - ) - } - - ($ObjectGraph | Get-Node BookStore~Title=*PowerShell*..Price).Value = 24.95 - $ObjectGraph | ConvertTo-Expression + $ObjectGraph = @{ BookStore = @( @{ Book = @{ - Price = 29.99 Title = 'Harry Potter' + Price = 29.99 } }, @{ Book = @{ - Price = 24.95 Title = 'Learning PowerShell' + Price = 39.95 } } ) } - for more details, see: [PowerShell Object Parser][1] and [Extended dot notation][2] + ($ObjectGraph | Get-Node BookStore~Title=*PowerShell*..Price).Value = 24.95 + $ObjectGraph | ConvertTo-Expression + @{ + BookStore = @( + @{ + Book = @{ + Price = 29.99 + Title = 'Harry Potter' + } + }, + @{ + Book = @{ + Price = 24.95 + Title = 'Learning PowerShell' + } + } + ) + } + +for more details, see: [PowerShell Object Parser][1] and [Extended dot notation][2] .PARAMETER InputObject - The concerned object graph or node. +The concerned object graph or node. .PARAMETER Path - Specifies the path to a specific node in the object graph. - The path might be either: +Specifies the path to a specific node in the object graph. +The path might be either: - * A dot-notation (`[String]`) literal or expression (as natively used with PowerShell) - * A array of strings (dictionary keys or Property names) and/or integers (list indices) - * A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object +* A dot-notation (`[String]`) literal or expression (as natively used with PowerShell) +* A array of strings (dictionary keys or Property names) and/or integers (list indices) +* A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object .PARAMETER Literal - If Literal switch is set, all (map) nodes in the given path are considered literal. +If Literal switch is set, all (map) nodes in the given path are considered literal. .PARAMETER ValueOnly - returns the value of the node instead of the node itself. +returns the value of the node instead of the node itself. .PARAMETER Unique - Specifies that if a subset of the nodes has identical properties and values, - only a single node of the subset should be selected. +Specifies that if a subset of the nodes has identical properties and values, +only a single node of the subset should be selected. .PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: +Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. +The failsafe will prevent infinitive loops for circular references as e.g. in: - $Test = @{Guid = New-Guid} - $Test.Parent = $Test + $Test = @{Guid = New-Guid} + $Test.Parent = $Test - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. +The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. +> [!Note] +> The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node +> at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" #> [Alias('gn')] diff --git a/Source/Cmdlets/Get-SortObjectGraph.ps1 b/Source/Cmdlets/Get-SortObjectGraph.ps1 index c7cabc6..42454e3 100644 --- a/Source/Cmdlets/Get-SortObjectGraph.ps1 +++ b/Source/Cmdlets/Get-SortObjectGraph.ps1 @@ -1,5 +1,3 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS Sort an object graph diff --git a/Source/Cmdlets/Import-ObjectGraph.ps1 b/Source/Cmdlets/Import-ObjectGraph.ps1 index b57cc92..7b340ac 100644 --- a/Source/Cmdlets/Import-ObjectGraph.ps1 +++ b/Source/Cmdlets/Import-ObjectGraph.ps1 @@ -1,62 +1,60 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Deserializes a PowerShell File or any object-graphs from PowerShell file to an object. +Deserializes a PowerShell File or any object-graphs from PowerShell file to an object. .DESCRIPTION - The `Import-ObjectGraph` cmdlet safely converts a PowerShell formatted expression contained by a file - to an object-graph existing of a mixture of nested arrays, hash tables and objects that contain a list - of strings and values. +The `Import-ObjectGraph` cmdlet safely converts a PowerShell formatted expression contained by a file +to an object-graph existing of a mixture of nested arrays, hash tables and objects that contain a list +of strings and values. .PARAMETER Path - Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. - Wildcard characters are permitted. +Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. +Wildcard characters are permitted. .PARAMETER LiteralPath - Specifies a path to one or more locations that contain a PowerShell the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. +Specifies a path to one or more locations that contain a PowerShell the object-graph. +The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. +If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell +PowerShell not to interpret any characters as escape sequences. .PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] +Defines which object types are allowed for the deserialization, see: [About language modes][2] - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. +* Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, + `[String]`, `[Array]` or `[HashTable]`. +* Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any - other files, which usually concerns PowerShell (`.ps1`) files. +The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any +other files, which usually concerns PowerShell (`.ps1`) files. - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. +> [!Caution] +> +> In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, +> CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. +> +> Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. +> Verify that the class types in the expression are safe before instantiating them. In general, it is +> best to design your configuration expressions with restricted or constrained classes, rather than +> allowing full freeform expressions. .PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given list type. +If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or +denied type initializer will be converted to the given list type. .PARAMETER MapAs - If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given map (dictionary or object) type. +If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or +denied type initializer will be converted to the given map (dictionary or object) type. - The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and - a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that - support explicit type initiators. +The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and +a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that +support explicit type initiators. .PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. +Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. .LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" +[1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" +[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" #> [Alias('Import-Object', 'imo')] diff --git a/Source/Cmdlets/Merge-ObjectGraph.ps1 b/Source/Cmdlets/Merge-ObjectGraph.ps1 index 9082cd6..05fd54a 100644 --- a/Source/Cmdlets/Merge-ObjectGraph.ps1 +++ b/Source/Cmdlets/Merge-ObjectGraph.ps1 @@ -1,38 +1,36 @@ -using module .\..\..\..\ObjectGraphTools - <# .SYNOPSIS - Merges two object graphs into one +Merges two object graphs into one .DESCRIPTION - Recursively merges two object graphs into a new object graph. +Recursively merges two object graphs into a new object graph. .PARAMETER InputObject - The input object that will be merged with the template object (see: [-Template] parameter). +The input object that will be merged with the template object (see: [-Template] parameter). - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: +> [!NOTE] +> Multiple input object might be provided via the pipeline. +> The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. +> To avoid a list of (root) objects to unroll, use the **comma operator**: - ,$InputObject | Compare-ObjectGraph $Template. + ,$InputObject | Compare-ObjectGraph $Template. .PARAMETER Template - The template that is used to merge with the input object (see: [-InputObject] parameter). +The template that is used to merge with the input object (see: [-InputObject] parameter). .PARAMETER PrimaryKey - In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to - link the items or properties: if the PrimaryKey exists on both the [-Template] and the - [-InputObject] and the values are equal, the dictionary or PowerShell object will be merged. - Otherwise (if the key can't be found or the values differ), the complete dictionary or - PowerShell object will be added to the list. +In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to +link the items or properties: if the PrimaryKey exists on both the [-Template] and the +[-InputObject] and the values are equal, the dictionary or PowerShell object will be merged. +Otherwise (if the key can't be found or the values differ), the complete dictionary or +PowerShell object will be added to the list. - It is allowed to supply multiple primary keys where each primary key will be used to - check the relation between the [-Template] and the [-InputObject]. +It is allowed to supply multiple primary keys where each primary key will be used to +check the relation between the [-Template] and the [-InputObject]. .PARAMETER MaxDepth - The maximal depth to recursively compare each embedded node. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). +The maximal depth to recursively compare each embedded node. +The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). #> [Alias('Merge-Object', 'mgo')] diff --git a/Source/Cmdlets/Test-ObjectGraph.ps1 b/Source/Cmdlets/Test-ObjectGraph.ps1 index 52faa76..76beef0 100644 --- a/Source/Cmdlets/Test-ObjectGraph.ps1 +++ b/Source/Cmdlets/Test-ObjectGraph.ps1 @@ -1,9 +1,8 @@ -using module .\..\..\..\ObjectGraphTools - using namespace System.Management.Automation using namespace System.Management.Automation.Language using namespace System.Collections using namespace System.Collections.Generic +using namespace System.Collections.Specialized <# .SYNOPSIS @@ -17,10 +16,10 @@ The schema object has the following major features: * Independent of the object notation (as e.g. [Json (JavaScript Object Notation)][2] or [PowerShell Data Files][3]) * Each test node is at the same level as the input node being validated -* Complex node requirements (as mutual exclusive nodes) might be selected using a logical formula +* Complex node Conditions (as mutual exclusive nodes) might be selected using a logical formula .EXAMPLE -#Test whether a `$Person` object meats the schema requirements. +#Test whether a `$Person` object meats the schema Conditions. $Person = [PSCustomObject]@{ FirstName = 'John' @@ -91,718 +90,913 @@ If set, the cmdlet will stop at the first invalid node and return the test resul .PARAMETER Elaborate -If set, the cmdlet will return the test result object for all tested nodes, even if they are valid +If set, the cmdlet will return the test result object for all nodes, even if they are valid or ruled out in a possible list node branch selection. -.PARAMETER AssertTestPrefix +.PARAMETER AssertPrefix -The prefix used to identify the assert test nodes in the schema object. By default, the prefix is `AssertTestPrefix`. +The prefix used to identify the Assert nodes in the schema object. By default, the prefix is `@`. .PARAMETER MaxDepth -The maximal depth to recursively test each embedded node. +The Maximum depth to recursively test each embedded node. The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). .LINK [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/SchemaObject.md "Schema object definitions" - #> +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ContainsOptionalTests', Justification = 'https://github.com/PowerShell/PSScriptAnalyzer/issues/1163')] [Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] +[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri = 'https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] +param( + [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] $InputObject, - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] + [Parameter(Mandatory = $true, Position = 0)] $SchemaObject, - [Parameter(ParameterSetName='ValidateOnly')] + [Parameter(ParameterSetName = 'ValidateOnly')] [Switch]$ValidateOnly, - [Parameter(ParameterSetName='ResultList')] + [Parameter(ParameterSetName = 'ResultList')] [Switch]$Elaborate, - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [ValidateNotNullOrEmpty()][String]$AssertTestPrefix = 'AssertTestPrefix', + [ValidateNotNullOrEmpty()][String]$AssertPrefixNodeName = 'AssertPrefix', - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth ) begin { - $Script:Yield = { - $Name = "$Args" -Replace '\W' - $Value = Get-Variable -Name $Name -ValueOnly -ErrorAction SilentlyContinue - if ($Value) { "$args" } - } - - $Script:Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } + $Script:UniqueCollections = @{} # The maximum schema object depth is bound by the input object depth (+1 one for the leaf test definition) $SchemaNode = [PSNode]::ParseInput($SchemaObject, ($MaxDepth + 2)) # +2 to be safe - $Script:AssertPrefix = if ($SchemaNode.Contains($AssertTestPrefix)) { $SchemaNode.Value[$AssertTestPrefix] } else { '@' } + $Script:AssertPrefix = if ($SchemaNode.Contains($AssertPrefixNodeName)) { $SchemaNode.Value[$AssertPrefixNodeName] } else { '@' } - function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } + class CheckBox { + [Nullable[Bool]]$NullableBool - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$([String]$SchemaNode) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object + CheckBox([Nullable[Bool]]$NullableBool) { $this.NullableBool = $NullableBool } + + [string] ToString() { + return $( + switch ($this.NullableBool) { + $null { [CommandColor][InverseColor]'[?]' } + $false { [ErrorColor][InverseColor]'[X]' } + $true { [VariableColor][InverseColor]'[V]' } + } + ) + } } - $Script:Tests = @{ - Description = 'Describes the test node' - References = 'Contains a list of assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Required = 'The node is required' - Unique = 'The node is unique' - - Minimum = 'The value is greater than or equal to' - ExclusiveMinimum = 'The value is greater than' - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - - MinimumLength = 'The value length is greater than or equal to' - Length = 'The value length is equal to' - MaximumLength = 'The value length is less than or equal to' - - MinimumCount = 'The node count is greater than or equal to' - Count = 'The node count is equal to' - MaximumCount = 'The node count is less than or equal to' - - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' + class Permutations : IEnumerator { + # This class enumerates all permutations of $n elements that Permutation over $i containers - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - AllowExtraNodes = 'Allow extra nodes' - } + [int[]] $Element + [List[int][]] $Container - $At = @{} - $Tests.Get_Keys().Foreach{ $At[$_] = "$($AssertPrefix)$_" } + Permutations([int]$ElementCount, [int]$ContainerCount) { + $this.Element = [int[]]::new($ElementCount) + $this.Container = [List[int][]]::new($ContainerCount) + } + + [object]get_Current() { + if ($null -eq $this.Container[0]) { return @() } + return $this.Container + } + + [bool] MoveNext() { + if ($this.Element.Count -eq 0 -or $this.Container.Count -eq 0) { return $false } + if ($null -eq $this.Container[0]) { + # First iteration + $this.Container[0] = [List[int]] (0..($this.Element.Count - 1)) + for ($i = 1; $i -lt $this.Container.Count; $i++) { $this.Container[$i] = [List[int]]::new() } + return $true + } + $n = 0 + while ($n -lt $this.Element.Count) { + $i = $this.Element[$n] # Container index + if (-not $this.Container[$i].Remove($n)) { throw "Container $i ($($this.Container[$i])) doesn't contain $n." } + $Carry = ++$i -ge $this.Container.Count + if ($Carry) { $i = 0 } + $this.Element[$n] = $i + $this.Container[$i].Add($n) + if (-not $Carry) { return $true } + $n++ + } + $this.Reset() + return $false + } + + [List[int][]] Copy() { + $Copy = [List[int][]]::new($this.Container.Count) + for ($i = 0; $i -lt $this.Container.Count; $i++) { + $Copy[$i] = [List[int]]::new($this.Container[$i]) + } + return $Copy + } - function ResolveReferences($Node) { - if ($Node.Cache.ContainsKey('TestReferences')) { return } + [void] Reset() { + $this.Element.Clear() + $this.Container.Clear() + } + [void] Dispose() {} } - function GetReference($LeafNode) { - $TestNode = $LeafNode.ParentNode - $References = if ($TestNode) { - if (-not $TestNode.Cache.ContainsKey('TestReferences')) { - $Stack = [Stack]::new() - while ($true) { - $ParentNode = $TestNode.ParentNode - if ($ParentNode -and -not $ParentNode.Cache.ContainsKey('TestReferences')) { - $Stack.Push($TestNode) - $TestNode = $ParentNode - continue - } - $RefNode = if ($TestNode.Contains($At.References)) { $TestNode.GetChildNode($At.References) } - $TestNode.Cache['TestReferences'] = [HashTable]::new($Ordinal[[Bool]$RefNode.CaseMatters]) - if ($RefNode) { - foreach ($ChildNode in $RefNode.ChildNodes) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($ChildNode.Name)) { - $TestNode.Cache['TestReferences'][$ChildNode.Name] = $ChildNode - } - } - } - $ParentNode = $TestNode.ParentNode - if ($ParentNode) { - foreach ($RefName in $ParentNode.Cache['TestReferences'].get_Keys()) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($RefName)) { - $TestNode.Cache['TestReferences'][$RefName] = $ParentNode.Cache['TestReferences'][$RefName] - } - } - } - if ($Stack.Count -eq 0) { break } - $TestNode = $Stack.Pop() + class Permutation : Permutations , IEnumerator[int] { + Permutation([int]$ElementCount, [int]$ContainerCount) : base($ElementCount, $ContainerCount) {} + [Int]get_Current() { return $this } + [string]ToString() { + return $( + foreach ($Indices in $this.Container) { + $Indices -join ',' } - } - $TestNode.Cache['TestReferences'] - } else { @{} } - if ($References.Contains($LeafNode.Value)) { - $AssertNode.Cache['TestReferences'] = $References - $References[$LeafNode.Value] + ) -join '|' } - else { SchemaError "Unknown reference: $LeafNode" $ObjectNode $LeafNode } } - function MatchNode ( - [PSNode]$ObjectNode, - [PSNode]$TestNode, - [Switch]$ValidateOnly, - [Switch]$Elaborate, - [Switch]$Ordered, - [Nullable[Bool]]$CaseSensitive, - [Switch]$MatchAll, - $MatchedNames - ) { - $Violates = $null - $Name = $TestNode.Name + class TestStage { + static [Bool]$Debug + + [PSNode]$ObjectNode + [Bool]$Report + [Bool]$Elaborate + [Int]$Depth + [TestStage]$ParentStage + [Bool]$Passed = $true + [Nullable[Bool]]$CaseSensitive + [Int]$FailCount + [List[Object]]$Results + + TestStage([PSNode]$ObjectNode, [Bool]$Elaborate, [Bool]$Report, [int]$Depth) { + if ($Elaborate -and -not $Report) { throw "Can't elaborate in validate-only mode" } + $this.ObjectNode = $ObjectNode + $this.Elaborate = $Elaborate + $this.Report = $Report + $this.Depth = $Depth + if ([TestStage]::Debug) { $this.WriteDebug($null, $null) } + } - $ChildNodes = $ObjectNode.ChildNodes - if ($ChildNodes.Count -eq 0) { return } + [TestStage]Create([PSNode]$Node, [bool]$Scan) { + $TestStage = [TestStage]::new($Node, $this.Elaborate, $this.Report, ($this.Depth + 1)) + $TestStage.ParentStage = $this.ParentStage + $TestStage.CaseSensitive = $this.CaseSensitive + if (-not $this.Report) { return $TestStage } # Validate mode: output no results + # if ($this.Elaborate) { return $TestStage } # Elaborate mode: output all results + if ($Scan) { $TestStage.Results = [List[Object]]::new() } # (Re)start scan stage + elseif ($this.Results -is [IList]) { $TestStage.Results = $this.Results } # Add scan results to parent + return $TestStage + } - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } + hidden WriteDebug($TestNode, [String]$Issue) { + $Indent = ' ' * ($this.Depth * 2) + $Line = (Get-PSCallStack).Where({ $_.Command }, 'First').ScriptLineNumber + $Prompt = if ($this.Report) { 'Report' } else { 'Validate' } + if ($null -ne $this.Results) { $Prompt += '(Scan)' } + $Node = $this.ObjectNode + if ($TestNode -is [PSNode]) { + Write-Host "$Indent$($Line):$Prompt>$($Node)?$($TestNode.Name)=$TestNode" ([CheckBox]$this.passed) $Issue + } + elseif ($Issue) { + Write-Host "$Indent$($Line):$Prompt>$($Node.Path)=$Issue" + } + else { + Write-Host "$Indent$($Line):$Prompt>$($Node.Path)=$Node" + } + } - if ($ObjectNode -is [PSMapNode] -and $TestNode.NodeOrigin -eq 'Map') { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "The node $Name is not in order" + [Object]Check([PSNode]$TestNode, [String]$Issue, [Bool]$Passed) { + if (-not $Passed) { + $this.Passed = $false + $this.FailCount++ + } + if ([TestStage]::Debug) { $this.WriteDebug($TestNode, $Issue) } + if (-not $this.Elaborate -and ($Passed -or -not $this.Report)) { return @() } + $Result = [PSCustomObject]@{ + ObjectNode = $this.ObjectNode + SchemaNode = $TestNode + Valid = $Passed + Issue = $Issue + } + $Result.PSTypeNames.Insert(0, 'TestResult') + if ($null -eq $this.Results) { return $Result } + $this.Results.Add($Result) + return @() # return nothing (enumerable null) + } + + [Object]AddResults ([List[Object]]$Results) { + if (-not $Results) { return @() } + if ($null -eq $this.Results) { return $Results } + $this.Results.AddRange($Results) + return @() + } + + hidden [String]GroupDesignate($Stages, $TestIndex, $Permutation) { + return $( + if ($Permutation[$TestIndex].Count -eq 0) { [CheckBox]::new($false) } + else { + foreach ($NodeIndex in $Permutation[$TestIndex]) { + $Check = if ($Stages[$NodeIndex] -and $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $Stages[$NodeIndex][$TestIndex].Passed + } + "$($this.ObjectNode.ChildNodes[$NodeIndex])$([CheckBox]::new($Check))" + } } - } else { $ChildNode = $false } + ) -join ',' } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Expected at least $($TestNodes.Count) (ordered) nodes" + + [iDictionary]GetDesignates ($Stages, $SubTests, $Permutation, $TestPassed) { + $Dictionary = [Dictionary[String, String]]::new() + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + $Name = $SubTests[$TestIndex].Name + $Dictionary[$Name] = [CheckBox]::new($TestPassed[$TestIndex]).ToString() + '=' + + $this.GroupDesignate($Stages, $TestIndex, $Permutation) } - $ChildNode = $ChildNodes[$NodeIndex] + return $Dictionary } - else { $ChildNode = $null } - if ($Violates) { - if (-not $ValidateOnly) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $AssertNode - Valid = -not $Violates - Issue = $Violates + [iList]ListDesignates ($Stages, $SubTests, $Permutation, $TestPassed) { + return @( + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + if ($TestPassed.ContainsKey($TestIndex)) { continue } + $SubTests[$TestIndex].Name + + [CheckBox]::new($TestPassed[$TestIndex]).ToString() + '=' + + $this.GroupDesignate($Stages, $TestIndex, $Permutation) } - $Output.PSTypeNames.Insert(0, 'TestResult') - $Output - } - return + ) } - if ($ChildNode -is [PSNode]) { - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Issue - } - TestNode @TestParams - if (-not $Issue) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - $SingleIssue = $Null - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Issue - } - TestNode @TestParams - if($Issue) { - if ($Elaborate) { $Issue } - elseif (-not $ValidateOnly -and $MatchAll) { - if ($null -eq $SingleIssue) { $SingleIssue = $Issue } else { $SingleIssue = $false } + + [String]Casing() { if ($this.CaseSensitive) { return '(case sensitive) ' } else { return '' } } + } + + [TestStage]::Debug = $DebugPreference -in 'Stop', 'Continue', 'Inquire' + + function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { + if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } + elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } + $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) + } + + function SchemaError($Message, $SchemaNode) { + $Exception = [ArgumentException]"$([String]$SchemaNode) $Message" + StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $SchemaNode + } + + $Script:Asserts = @{ + Description = 'Describes the test node' + References = 'Contains a list of Assert references' + CaseSensitive = 'The (descendant) nodes are considered case sensitive' + Unique = '_ObjectValue_ is unique' + + Ordered = 'The nodes are in order' + AnyName = 'Any map name is allowed' + Requires = 'The node requirement is met' + + Optional = 'The map node is optional' + Count = 'The list node exists _SchemaValue_ times' + MinimumCount = 'The list node exists at least _SchemaValue_ times' + MaximumCount = 'The list node exists at most _SchemaValue_ times' + + Type = '_ObjectValue_ is of type _SchemaValue_' + NotType = '_ObjectValue_ is not type _SchemaValue_' + + Minimum = '_ObjectValue_ is greater than or equal to _SchemaValue_' + ExclusiveMinimum = '_ObjectValue_ is greater than _SchemaValue_' + ExclusiveMaximum = '_ObjectValue_ is less than _SchemaValue_' + Maximum = '_ObjectValue_ is less than or equal to _SchemaValue_' + + MinimumLength = '_ObjectValue_ length is greater than or equal to _SchemaValue_' + Length = '_ObjectValue_ length is equal to _SchemaValue_' + MaximumLength = '_ObjectValue_ length is less than or equal to _SchemaValue_' + + Like = '_ObjectValue_ is like _SchemaValue_' + Match = '_ObjectValue_ matches _SchemaValue_' + NotLike = '_ObjectValue_ is not like _SchemaValue_' + NotMatch = '_ObjectValue_ not matches _SchemaValue_' + } + + $At = @{} + $Asserts.Get_Keys().Foreach{ $At[$_] = "$($Script:AssertPrefix)$_" } + + function GetLink($LeafNode) { + # A test node with a string value is a reference to another node + # described in a ancestor @Reference map node + $ParentNode = $LeafNode.ParentNode + if (-not $ParentNode.Cache.ContainsKey('@References')) { + $Stack = [Stack]::new() + while ($ParentNode -and -not $ParentNode.Cache.ContainsKey('@References')) { + $Stack.Push($ParentNode) + $ParentNode = $ParentNode.ParentNode + } + $References = if ($ParentNode -and $ParentNode.Cache.ContainsKey('@References')) { $ParentNode.Cache['@References'] } else { @{} } + while ($Stack.Count) { + $ParentNode = $Stack.Pop() + if ($ParentNode.Contains($At.References)) { + $Inherit = $References + $RefNode = $ParentNode.GetChildNode($At.References) + if ($RefNode -is [PSMapNode]) { + $Ordinal = if ($RefNode.CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + $References = [HashTable]::new($Ordinal) + foreach ($Node in $RefNode.ChildNodes) { $References[$Node.Name] = $Node } + } + else { SchemaError "The reference node should be a map node" $RefNode } + foreach ($Key in $Inherit.get_Keys()) { + if (-not $References.ContainsKey($Key)) { $References[$Key] = $Inherit[$Key] } } } - else { - $null = $MatchedNames.Add($ChildNode.Name) - if (-not $MatchAll) { break } - } + $ParentNode.Cache['@References'] = $References } - if ($SingleIssue) { $SingleIssue } } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } + $Reference = $ParentNode.Cache['@References'][$LeafNode.Value] + if ($Reference) { $Reference } else { SchemaError "Unknown reference: $LeafNode" $LeafNode } } function TestNode ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$Elaborate, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity frm the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node + [TestStage]$TestStage, + [PSCollectionNode]$SchemaNode ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ParameterColor]'Caller (line: $($Caller.ScriptLineNumber))'):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ParameterColor]'ObjectNode:')" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ParameterColor]'SchemaNode:')" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ParameterColor]'ValidateOnly:')" ([Bool]$ValidateOnly) - } - if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # Allow any node + $ObjectNode = $TestStage.ObjectNode + if ($ObjectNode -is [PSLeafNode]) { $SubNodes = @() } else { $SubNodes = $ObjectNode.ChildNodes } + if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # @() = Allow any node, @{} = Deny any node $AssertValue = $ObjectNode.Value - $RefInvalidNode.Value = $null # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} # $AssertNodes{] = $ChildNodes.@ + $ExtraTest, $Condition, $Ordered, $CaseMatters, $Ordinal = $null + $CaseMatters = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } + $Ordinal = if ($CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + $SubTests = [OrderedDictionary]::new($Ordinal) + $AssertNodes = [Ordered]@{} # $AssertNodes[] = $SubNodes.@ if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() foreach ($Node in $SchemaNode.ChildNodes) { - if ($Null -eq $Node.Parent -and $Node.Name -eq $AssertTestPrefix) { continue } - if ($Node.Name.StartsWith($AssertPrefix)) { - $TestName = $Node.Name.SubString($AssertPrefix.Length) - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown assert: '$($Node.Name)'" $ObjectNode $SchemaNode } + if ($Null -eq $Node.ParentNode.ParentNode -and $Node.Name -eq $AssertPrefixNodeName) { continue } + if ($Node.Name -is [String] -and $Node.Name.StartsWith($Script:AssertPrefix)) { + $TestName = $Node.Name.SubString($Script:AssertPrefix.Length) + if ($TestName -notin $Asserts.Keys) { SchemaError "Unknown Assert: '$TestName'" $SchemaNode } $AssertNodes[$TestName] = $Node } - else { $TestNodes.Add($Node) } + else { + $SubTests[[Object]$Node.Name] = if ($Node -is [PSLeafNode]) { GetLink $Node } else { $Node } + } } } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - if ($AssertNodes.Contains('CaseSensitive')) { $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] } - $AllowExtraNodes = if ($AssertNodes.Contains('AllowExtraNodes')) { $AssertNodes['AllowExtraNodes'] } + elseif ($SchemaNode -is [PSListNode]) { + foreach ($Node in $SchemaNode.ChildNodes) { $SubTests[[Object]$Node.Name] = $Node } + } -#Region Node validation + if ($AssertNodes.Contains('CaseSensitive')) { $TestStage.CaseSensitive = $AssertNodes['CaseSensitive'] } - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null + $LeafTest = $false foreach ($TestName in $AssertNodes.get_Keys()) { - $AssertNode = $AssertNodes[$TestName] - $Criteria = $AssertNode.Value - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Description') { $Null } - elseif ($TestName -eq 'References') { } + + $TestNode = $AssertNodes[$TestName] + $TestValue = $TestNode.Value + + #Region Node Asserts + + if ($TestName -in 'Description', 'References', 'Optional', 'Count', 'MinimumCount', 'MaximumCount') { + continue + } + elseif ($TestName -eq 'CaseSensitive') { + if ($null -ne $TestValue -and $TestValue -isnot [Bool]) { + SchemaError "The case sensitivity value should be a boolean: $TestValue" $SchemaNode + } + continue + } + elseif ($TestName -eq 'Ordered') { + if ($TestValue -is [Bool]) { $Ordered = [Bool]$TestValue } + else { SchemaError "The ordered assert should be a boolean" $SchemaNode } + if ($ObjectNode -isnot [PSCollectionNode]) { + $TestStage.Check($TestNode, "The $ObjectNode is not a collection node", $false) + } + continue + } + elseif ($TestName -eq 'AnyName') { + $ExtraTest = $TestNode + if (-not $SubTests) { [OrderedDictionary]::new() } + continue + } + elseif ($TestName -eq 'Requires') { + if ($Ordered) { SchemaError "A ordered collection cannot have a Requires assert" $SchemaNode } + if ($ObjectNode -is [PSCollectionNode]) { + $Condition = [LogicalFormula]::new() + $TestValue.foreach{ $Condition.And([LogicalFormula]$_) } # 'a or b', 'c or d' --> 'a or b and (c or d)' + } + else { $TestStage.Check($TestNode, "The node $ObjectNode is not a collection node", $false) } + continue + } elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { + $TypeName = $null + $FoundType = foreach ($TypeName in $TestValue) { if ($TypeName -in $null, 'Null', 'Void') { if ($null -eq $AssertValue) { $true; break } } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } + else { + $Type = if ($TypeName -is [Type]) { $TypeName } else { $TypeName -as [Type] } + if (-not $Type) { SchemaError "Unknown type: $TypeName" $TypeName } + if ($ObjectNode -is $Type -or $AssertValue -is $Type) { $true; break } } - if ($ObjectNode -is $Type -or $AssertValue -is $Type) { $true; break } } $Not = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - if ($null -eq $FoundType -xor $Not) { $Violates = "The node or value is $(if (!$Not) { 'not ' })of type $AssertNode" } - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "The case sensitivity value should be a boolean: $Criteria" $ObjectNode $SchemaNode + if ($null -eq $FoundType -xor $Not) { + $DisplayType = $([TypeColor][PSSerialize]::new($TypeName, [PSLanguageMode]'NoLanguage')) + $TestStage.Check($TestNode, "$($ObjectNode.DisplayValue) is $(if (!$Not) { 'not ' })of type $DisplayType", $false) } } elseif ($TestName -in 'Minimum', 'ExclusiveMinimum', 'ExclusiveMaximum', 'Maximum') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "The $ObjectNode is not a string or value type", $false) } elseif ($TestName -eq 'Minimum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cle $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ile $Value } + else { $TestValue -le $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less or equal than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())less or equal than $TestValue", $false) } } elseif ($TestName -eq 'ExclusiveMinimum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -clt $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ilt $Value } + else { $TestValue -lt $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())less than $TestValue", $false) } } elseif ($TestName -eq 'ExclusiveMaximum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cgt $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -igt $Value } + else { $TestValue -gt $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())greater than $TestValue", $false) } } - else { # if ($TestName -eq 'Maximum') { + else { + # if ($TestName -eq 'Maximum') { $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } + if ($TestStage.CaseSensitive -eq $true) { $TestValue -cge $Value } + elseif ($TestStage.CaseSensitive -eq $false) { $TestValue -ige $Value } + else { $TestValue -ge $Value } if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" + $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is $($TestStage.Casing())greater or equal than $TestValue)", $false) } } - if ($Violates) { break } + if (-not $TestStage.Passed) { break } } } - elseif ($TestName -in 'MinimumLength', 'Length', 'MaximumLength') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "The $ObjectNode is not a string or value type", $false) break } $Length = "$Value".Length if ($TestName -eq 'MinimumLength') { - if ($Length -lt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is less than $AssertNode" + if ($Length -lt $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is less than $TestValue", $false) } } elseif ($TestName -eq 'Length') { - if ($Length -ne $Criteria) { - $Violates = "The string length of '$Value' ($Length) is not equal to $AssertNode" + if ($Length -ne $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is not equal to $TestValue", $false) } } - else { # if ($TestName -eq 'MaximumLength') { - if ($Length -gt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is greater than $AssertNode" + else { + # if ($TestName -eq 'MaximumLength') { + if ($Length -gt $TestValue) { + $TestStage.Check($TestNode, "The string length of $($ObjectNode.DisplayValue) ($Length) is greater than $TestValue", $false) } } - if ($Violates) { break } + if (-not $TestStage.Passed) { break } } } elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } + $LeafTest = $true $Negate = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - $Match = $TestName.EndsWith('Match', 'OrdinalIgnoreCase') + $Match = $TestName.EndsWith('Match', 'OrdinalIgnoreCase') $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } + if (-not $ValueNodes) { $TestStage.Check($TestNode, "The node $ObjectNode is empty", $false) } foreach ($ValueNode in $ValueNodes) { $Value = $ValueNode.Value if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" + $TestStage.Check($TestNode, "$ObjectNode is not a string or value type", $false) break } $Found = $false - foreach ($AnyCriteria in $Criteria) { + $Criteria = $null + foreach ($Criteria in $TestValue) { $Found = if ($Match) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } + if ($true -eq $TestStage.CaseSensitive) { $Value -cmatch $Criteria } + elseif ($false -eq $TestStage.CaseSensitive) { $Value -imatch $Criteria } + else { $Value -match $Criteria } } - else { # if ($TestName.EndsWith('Link', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } + else { + if ($true -eq $TestStage.CaseSensitive) { $Value -clike $Criteria } + elseif ($false -eq $TestStage.CaseSensitive) { $Value -ilike $Criteria } + else { $Value -like $Criteria } } if ($Found) { break } } $IsValid = $Found -xor $Negate if (-not $IsValid) { - $Not = if (-Not $Negate) { ' not' } - $Violates = - if ($Match) { "The $(&$Yield '(case sensitive) ')value $Value does$not match $AssertNode" } - else { "The $(&$Yield '(case sensitive) ')value $Value is$not like $AssertNode" } + $Not = if (-not $Negate) { ' not' } + $DisplayCriteria = $([TypeColor][PSSerialize]::new($Criteria, [PSLanguageMode]'NoLanguage')) + if ($Match) { $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) does$not $($TestStage.Casing())match $DisplayCriteria", $false) } + else { $TestStage.Check($TestNode, "The value $($ObjectNode.DisplayValue) is$not $($TestStage.Casing())like $DisplayCriteria", $false) } } } } - - elseif ($TestName -in 'MinimumCount', 'Count', 'MaximumCount') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The node $ObjectNode is not a collection node" - } - elseif ($TestName -eq 'MinimumCount') { - if ($ChildNodes.Count -lt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is less than $AssertNode" - } - } - elseif ($TestName -eq 'Count') { - if ($ChildNodes.Count -ne $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is not equal to $AssertNode" - } - } - else { # if ($TestName -eq 'MaximumCount') { - if ($ChildNodes.Count -gt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is greater than $AssertNode" - } - } - } - - elseif ($TestName -eq 'Required') { } - elseif ($TestName -eq 'Unique' -and $Criteria) { + elseif ($TestName -eq 'Unique') { + if (-not $TestValue) { continue } if (-not $ObjectNode.ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode + SchemaError "The unique Assert can't be used on a root node" $SchemaNode } - if ($Criteria -eq $true) { $UniqueCollection = $ObjectNode.ParentNode.ChildNodes } - elseif ($Criteria -is [String]) { - if (-not $UniqueCollections.Contains($Criteria)) { - $UniqueCollections[$Criteria] = [List[PSNode]]::new() + if ($TestValue -eq $true) { $UniqueCollection = $ObjectNode.ParentNode.ChildNodes } + elseif ($TestValue -is [String]) { + if (-not $Script:UniqueCollections.Contains($TestValue)) { + $Script:UniqueCollections[$TestValue] = [List[PSNode]]::new() } - $UniqueCollection = $UniqueCollections[$Criteria] + $UniqueCollection = $Script:UniqueCollections[$TestValue] } - else { SchemaError "The unique assert value should be a boolean or a string" $ObjectNode $SchemaNode } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) + else { SchemaError "The unique assert value should be a boolean or a string" $SchemaNode } + $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$TestStage.CaseSensitive) foreach ($UniqueNode in $UniqueCollection) { if ([object]::ReferenceEquals($ObjectNode, $UniqueNode)) { continue } # Self if ($ObjectComparer.IsEqual($ObjectNode, $UniqueNode)) { - $Violates = "The node is equal to the node: $($UniqueNode.Path)" + $TestStage.Check($TestNode, "The $($ObjectNode.DisplayValue) is equal to the node: $($UniqueNode.Path)", $false) break } } - if ($Criteria -is [String]) { $UniqueCollection.Add($ObjectNode) } - } - elseif ($TestName -eq 'AllowExtraNodes') {} - elseif ($TestName -in 'Ordered', 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' is not a collection node" - } + if ($TestValue -is [String]) { $UniqueCollection.Add($ObjectNode) } } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } + else { SchemaError "Unhandled Assert: $TestName" $TestNode } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } + #EndRegion Node Asserts - if ($Violates -or $Elaborate) { - $Issue = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName]) } - else { "$($Tests[$TestName] -replace 'The value ', "The value $ObjectNode ") $AssertNode" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = $Issue - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } + if (-not $TestStage.Passed) { return } # Already issued + if ([TestStage]::Debug -or $TestStage.Elaborate) { + $Issue = $Asserts[$TestName] -replace '\b_ObjectValue_\b', $ObjectNode.DisplayValue -replace '\b_SchemaValue_\b', $TestNode.DisplayValue + $TestStage.Check($TestNode, $Issue, $true) } } -#EndRegion Node validation - - if ($Violates) { return } - -#Region Required nodes + if ($LeafTest) { return } # No child nodes to test - $ChildNodes = $ObjectNode.ChildNodes + #Region SubTests - if ($TestNodes.Count -and -not $AssertNodes.Contains('Type')) { - if ($SchemaNode -is [PSListNode] -and $ObjectNode -isnot [PSListNode]) { - $Violates = "The node $ObjectNode is not a list node" + if (-not $SubTests.Count) { + if ($Condition) { SchemaError "Expected a test node for each requirement" $SchemaNode } + if (-not $SubNodes) { return } + } + $FailCount = $TestStage.FailCount + $CaseMatters = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } + $Ordinal = if ($CaseMatters) { [StringComparer]::Ordinal } else { [StringComparer]::OrdinalIgnoreCase } + + $Required = if ($null -eq $Condition) { [LogicalFormula]::new() } + $RequiredNames = [HashSet[String]]::new($Ordinal) + if ($Condition) { $Condition.Find({ $Args[0] -is [LogicalVariable] }, $true).foreach{ $null = $RequiredNames.Add($_.Value) } } + + $ContainsOptionalTests = $false + $TestIndices = [Dictionary[string, int]]::new($Ordinal) + $MinimumCount = [Dictionary[int, int]]::new() + $MaximumCount = [Dictionary[int, int]]::new() + $TestIndex = 0 + foreach ($TestName in $SubTests.get_Keys()) { + #$TestName is the reference name, $SubTest.Name is the actual name of the test + $SubTest = $SubTests[$TestIndex] + $TestIndices[$SubTest.Name] = $TestIndex + $Optional = $SubTest.GetValue($At.Optional, $null) + $Count = $SubTest.GetValue($At.Count, $null) + $Minimum = $SubTest.GetValue($At.MinimumCount, $null) + $Maximum = $SubTest.GetValue($At.MaximumCount, $null) + if ($Optional -and ($Count -or $Minimum)) { + SchemaError "The Optional (for maps) and (Minimum)Count (for lists) asserts are mutual exclusive" $SubTest + } + elseif ($Count -and ($Minimum -or $Maximum)) { + SchemaError "The count and MinimumCount/MaximumCount asserts are mutual exclusive" $SubTest + } + if ($null -ne $Optional) { + if ($null -ne ($Bool = $Optional -as [Bool])) { + $MinimumCount[$TestIndex] = 1 - $Bool + } + else { SchemaError "The Optional assert should be a boolean type" $SubTest } } - if ($SchemaNode -is [PSMapNode] -and $ObjectNode -isnot [PSMapNode]) { - $Violates = "The node $ObjectNode is not a map node" + if ($null -ne $Count) { + if ($null -ne ($Int = $Count -as [UInt32])) { + $MinimumCount[$TestIndex] = $int + $MaximumCount[$TestIndex] = $int + } + else { SchemaError "The MinimumCount assert should be a positive integer type" $SubTest } } + if ($null -ne $Minimum) { + if ($null -ne ($Int = $Minimum -as [UInt32])) { + $MinimumCount[$TestIndex] = $int + } + else { SchemaError "The MinimumCount assert should be a positive integer type" $SubTest } + } + if ($null -ne $Maximum) { + if ($null -ne ($Int = $Maximum -as [UInt32])) { + $MaximumCount[$TestIndex] = $int + } + else { SchemaError "The MaximumCount assert should be a positive integer type" $SubTest } + } + if ($MinimumCount.ContainsKey($TestIndex)) { + if ($MinimumCount[$TestIndex]) { + if ($Required) { $Required.And($TestName) } + elseif ($RequiredNames.Add($TestName)) { $Condition.And($TestName) } + } + elseif ($Condition) { SchemaError "Required tests cannot be optional" $SubTest } + } + elseif ($Required) { + $MinimumCount[$TestIndex] = 1 + if ($RequiredNames.Add($TestName)) { $Required.And($TestName) } + } + elseif ($MinimumCount.ContainsKey($TestIndex)) { + $ContainsOptionalTests = $MinimumCount[$TestIndex] -eq 0 + } + else { $MinimumCount[$TestIndex] = 0 } + $TestIndex++ } - - if (-Not $Violates) { - $RequiredNodes = $AssertNodes['RequiredNodes'] - $CaseSensitiveNames = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } - $AssertResults = [HashTable]::new($Ordinal[[Bool]$CaseSensitiveNames]) - - if ($RequiredNodes) { $RequiredList = [List[Object]]$RequiredNodes.Value } else { $RequiredList = [List[Object]]::new() } - foreach ($TestNode in $TestNodes) { - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } - if ($AssertNode -is [PSMapNode] -and $AssertNode.GetValue($At.Required)) { $RequiredList.Add($TestNode.Name) } + if ($Condition) { $Required = $Condition } + # else { $Required.Find({ $Args[0] -is [LogicalVariable] }, $true).foreach{ $null = $RequiredNames.Add($_.Value) } } + if ($Ordered) { + if ($SubNodes.Count -lt $SubTests.Count) { + $TestStage.Check($SchemaNode, "There are less than $($SubTests.Count) (ordered) child nodes", $false) + return } + elseif ($SubNodes.Count -gt $SubTests.Count -and -not $ExtraTest) { + $TestStage.Check($SchemaNode, "There are more than $($SubTests.Count) (ordered) child nodes", $false) + return + } + if ($ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode]) { + for ($Index = 0; $Index -lt $SubTests.Count; $Index++) { + $NodeName = $SubNodes[$Index].Name + $TestName = $SubTests[$Index].Name + $EqualName = if ($CaseMatters) { $TestName -ceq $NodeName } else { $TestName -ieq $NodeName } + if (-not $EqualName) { + $TestStage.Check($SchemaNode, "Node #$Index ($([PSSerialize]$SubNodes[$Index].Name)) was not $([PSSerialize]$Name)", $false) + return + } + } + } + for ($Index = 0; $Index -lt $SubTests.Count; $Index++) { + $SubNode = $SubNodes[$Index] + $SubTest = $SubTests[$Index] + $ChildStage = $TestStage.Create($SubNode, $false) + TestNode $ChildStage $SubTest + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } + } + while ($Index -lt $SubNodes.Count) { + $SubNode = $SubNodes[$Index] + $ChildStage = $TestStage.Create($SubNode, $false) + TestNode $ChildStage $ExtraTest + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } + $Index++ + } + return + } + elseif ($ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode]) { + if ($SubNodes.Count -gt $SubTests.Count -and -not $ExtraTest) { + foreach ($ChildNode in $SubNodes) { + if ($SubTests.Contains($ChildNode.Name)) { continue } + $TestStage.Check($SchemaNode, "Node $([PSSerialize]$ChildNode.Name) is denied", $false) + } + return + } + if ($SubNodes) { $Names = $SubNodes.Name } else { $Names = @() } + if ($Required.Terms -and -not $Required.Evaluate($Names)) { + $TestStage.Check($SchemaNode, "The requirement { $Required } is not met", $false) + return + } + foreach ($ChildNode in $SubNodes) { + if ($SubTests.Contains($ChildNode.Name)) { $SubTest = $SubTests[[Object]$ChildNode.Name] } + elseif ($ExtraTest) { $SubTest = $ExtraTest } + else { + $TestStage.Check($SchemaNode, "Node $([PSSerialize]$ChildNode.Name) is denied", $false) + continue + } + $ChildStage = $TestStage.Create($ChildNode, $false) + TestNode $ChildStage $SubTest + if (-not $ChildStage.Passed) { $TestStage.Passed = $false } + } + return + } + # (unordered) $ObjectNode -is [PSListNode] -or $SchemaNode -is [PSListNode] + if ($ExtraTest) { $SubTests[[Object]'@AnyName'] = $ExtraTest } # The ExtraTest is just an optional test for a list + if ($SubTests.count -eq 1 -and $Required.Terms.count -le 1) { + # Single test (no scanning required) + $SubTest = $SubTests[0] + if ($SubNodes.Count -lt $MinimumCount[0]) { + $TestStage.Check($SubTest, "The node $ObjectNode contains less than $($MinimumCount[0]) child nodes", $false) + return + } + if ($MaximumCount.ContainsKey(0) -and $SubNodes.Count -gt $MaximumCount[0]) { + $TestStage.Check($SubTest, "The node $ObjectNode contains more than $($MaximumCount[0]) child nodes", $false) + return + } + foreach ($ChildNode in $SubNodes) { + $ChildStage = $TestStage.Create($ChildNode, $false) + TestNode $ChildStage $SubTest + $TestStage.Passed = $ChildStage.Passed + if (-not $ChildStage.Passed -and -not $TestStage.Report) { return } # Validate only + } + return + } + $Stages = [Object[]]::new($SubNodes.Count) + $TestStage.Passed = $false + $Best = $null + foreach ($Permutation in [Permutation]::new($SubNodes.Count, $SubTests.Count)) { + $Score = 0 + $TestPassed = [Dictionary[Int, Bool]]::new() + $UsedNodes = [HashSet[Int]]::new() + $RequiredPassed = if ($Required.Terms) { + $Required.Evaluate({ + $TestIndex = $TestIndices[$_] + if ($null -eq $TestIndex) { return $true } # Might happen at maxdepth + $SubTest = $SubTests[$TestIndex] + $Indices = $Permutation[$TestIndex] + $OutsideBounds = if ($Indices.Count -lt $MinimumCount[$TestIndex]) { + $Indices.Count - $MinimumCount[$TestIndex] + } + elseif ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $MaximumCount[$TestIndex] - $Indices.Count + } + if ($OutsideBounds) { + $Score -= $OutsideBounds + if (-not $TestStage.Report) { return $false } # Validate only + # if (-not $TestPassed.ContainsKey($TestIndex)) { return $false } + foreach ($NodeIndex in $Indices) { # Add score based on what is known + if (-not $Stages[$NodeIndex]) { continue } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { continue } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount } + } + return $false + } + if (-not $TestPassed.ContainsKey($TestIndex)) { $TestPassed[$TestIndex] = $false } + foreach ($NodeIndex in $Indices) { + # A logical variable might refer to multiple child nodes + if (-not $Stages[$NodeIndex]) { $Stages[$NodeIndex] = [Dictionary[int, TestStage]]::new() } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $ChildNode = $SubNodes[$NodeIndex] + $Stages[$NodeIndex][$TestIndex] = $TestStage.Create($ChildNode, $true) + TestNode $Stages[$NodeIndex][$TestIndex] $SubTest + } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount; return $false } # All child nodes need to fulfill the test + $null = $UsedNodes.Add($NodeIndex) - foreach ($Requirement in $RequiredList) { - $LogicalFormula = [LogicalFormula]$Requirement - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $null - Operator = $null - Negate = $null + } + $Score += 1 + $Indices.Count + $TestPassed[$TestIndex] = $true + return $true }) - $Term, $Operand, $Accumulator = $null - While ($Stack.Count -gt 0) { - # Accumulator = Accumulator Operand - # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Operator = $Pop.Operator - if ($null -eq $Operator) { $Operand = $Pop.Accumulator } - else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } - $Negate = $Pop.Negate - $Compute = $null -notin $Operand, $Operator, $Accumulator - while ($Compute -or $Enumerator.MoveNext()) { - if ($Compute) { $Compute = $false} - else { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $SchemaNode.GetChildNode($Name) - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = $false - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - $AssertResults[$Name] = $MatchedNames.Count -gt $MatchCount0 - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term.Value -eq 'Not') { $Negate = -Not $Negate } - elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } - else { SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode } - } - elseif ($Term -is [LogicalFormula]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator, $Operator, $Negate = $null - $Enumerator = $Term.Terms.GetEnumerator() - continue - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - $Negate = $null - if ($Operator -eq 'And') { - $Operator = $null - if ($Accumulator -eq $false -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - $Operator = $null - if ($Accumulator -eq $true -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Operator = $null - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand = $Null + } else { $true } + $NodesLeft = $SubNodes.Count - $UsedNodes.Count + $TestsLeft = $SubTests.Count - $TestPassed.Count + $OptionalPassed = if (-not $RequiredPassed) { $false } + elseif ($NodesLeft -and $TestsLeft) { $null } # else: one or both are zero + elseif (-not $NodesLeft) { $true } + elseif (-not $ContainsOptionalTests) { $false } + # $TestOptional = if ($RequiredPassed -and $NodesLeft) { + # if ($TestsLeft) { $ContainsOptionalTests } else { $RequiredPassed = $false } + # } + # $OptionalPassed = $null + # if ($TestOptional) { + if ($null -eq $OptionalPassed) { + for ($TestIndex = 0; $TestIndex -lt $SubTests.Count; $TestIndex++) { + if ($TestPassed.ContainsKey($TestIndex)) { continue } + if ($MinimumCount[$TestIndex]) { continue } # not optional (handled in condition evaluation) + $Indices = $Permutation[$TestIndex] + if ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $Score -= $Indices.Count - $MaximumCount[$TestIndex] + if (-not $TestStage.Report) { break } # Validate only + foreach ($NodeIndex in $Indices) { # Add score based on what is known + if (-not $Stages[$NodeIndex]) { continue } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { continue } + $Stage = $Stages[$NodeIndex][$TestIndex] + if ($Stage.Passed) { $Score++ } else { $Score -= 2 + $Stage.FailCount } } + $OptionalPassed = $false + break } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Operator" $ObjectNode $SchemaNode + foreach ($NodeIndex in $Indices) { + if (-not $Stages[$NodeIndex]) { $Stages[$NodeIndex] = [Dictionary[int, TestStage]]::new() } + if (-not $Stages[$NodeIndex].ContainsKey($TestIndex)) { + $ChildNode = $SubNodes[$NodeIndex] + $Stages[$NodeIndex][$TestIndex] = $TestStage.Create($ChildNode, $true) + TestNode $Stages[$NodeIndex][$TestIndex] $SubTests[$TestIndex] # $SubTest ??? + } + $Stage = $Stages[$NodeIndex][$TestIndex] + $OptionalPassed = $Stage.Passed + if ($OptionalPassed) { $Score++ } else { $Score -= 2 + $Stage.FailCount; break } # All (optional) child nodes need to fulfill the test } + if ($OptionalPassed -eq $false) { break } + $Score += 1 + $Indices.Count } - if ($Accumulator -eq $False) { - $Violates = "The required node condition $LogicalFormula is not met" - break + } else { $Score -= $NodesLeft } + if ($null -eq $OptionalPassed) { $OptionalPassed = $true } + + if ([TestStage]::Debug) { + $Better = if (-not $Best -or $Score -gt $Best['Score']) { ' (best)' } + $Designates = $TestStage.GetDesignates($Stages, $SubTests, $Permutation, $TestPassed) + $TestStage.WriteDebug($null, "$([ParameterColor]'Required')$([CheckBox]::new($RequiredPassed)):$($Required.ToString($Designates)) $([ParameterColor]""Score:$Score $Better"")") + if ($TestsLeft -and $NodesLeft) { + $Designates = $TestStage.ListDesignates($Stages, $SubTests, $Permutation, $TestPassed) -join ',' + $TestStage.WriteDebug($null, "$([ParameterColor]'Optional')$([CheckBox]::new($OptionalPassed)):$Designates") } } + $TestStage.Passed = $RequiredPassed -and $OptionalPassed + # $TestStage.Passed = $RequiredPassed -and $OptionalPassed + if ($TestStage.Passed) { break } + if (-not $Best -or $Score -gt $Best['Score']) { # Capture the highest score with the least amount of issues + if (-not $Best) { $Best = @{} } + $Best['Score'] = $Score + $Best['TestPassed'] = [Dictionary[Int, Bool]]::new($TestPassed) + $Best['Permutation'] = $foreach.Copy() + } } - -#EndRegion Required nodes - -#Region Optional nodes - - if (-not $Violates) { - - foreach ($TestNode in $TestNodes) { - if ($MatchedNames.Count -ge $ChildNodes.Count) { break } - if ($AssertResults.Contains($TestNode.Name)) { continue } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $TestNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = -not $AllowExtraNodes - MatchedNames = $MatchedNames + # if ([TestStage]::Debug) { + # $ScoreFail = [ParameterColor]"(Score:$($Best['Score']))" + # $Designates = $TestStage.GetDesignates($Stages, $SubTests, $Best['Permutation'], $Best['TestPassed']) + # $TestStage.WriteDebug($null, "Selected$([CheckBox]::new($RequiredPassed)):$($Required.ToString($Designates)) $ScoreFail") + # } + if ($TestStage.Elaborate) { + for ($NodeIndex = 0; $NodeIndex -lt $SubNodes.Count; $NodeIndex++) { + if (-not $Stages[$NodeIndex]) { continue } + foreach ($Stage in $Stages[$NodeIndex].get_Values()) { + $TestStage.AddResults($Stage.Results) } - MatchNode @MatchParams - if ($AllowExtraNodes -and $MatchedNames.Count -eq $MatchCount0) { - $Violates = "When extra nodes are allowed, the node $ObjectNode should be accepted" - break - } - $AssertResults[$TestNode.Name] = $MatchedNames.Count -gt $MatchCount0 } - - if (-not $AllowExtraNodes -and $MatchedNames.Count -lt $ChildNodes.Count) { - $Count = 0; $LastName = $Null - $Names = foreach ($Name in $ChildNodes.Name) { - if ($MatchedNames.Contains($Name)) { continue } - if ($Count++ -lt 4) { - if ($ObjectNode -is [PSListNode]) { [CommandColor]$Name } - else { [StringColor][PSKeyExpression]::new($Name, [PSSerialize]::MaxKeyLength)} + } + elseif ($TestStage.Passed) { return } + elseif ($Best) { + $Permutation = $Best['Permutation'] + for ($TestIndex = 0; $TestIndex -lt $Permutation.Count; $TestIndex++) { + $Indices = $Permutation[$TestIndex] + if ($Indices.Count -lt $MinimumCount[$TestIndex]) { + if ($MinimumCount[$TestIndex] -eq 1) { + $TestStage.Check($SchemaNode, "The requirement $([LogicalVariable]$SubTests[$TestIndex].Name) is missing", $false) } - else { $LastName = $Name } + else { + $TestStage.Check($SchemaNode, "$([LogicalVariable]$SubTests[$TestIndex].Name) occurred less than $($MinimumCount[$TestIndex]) times", $false) + } + } + if ($MaximumCount.ContainsKey($TestIndex) -and $Indices.Count -gt $MaximumCount[$TestIndex]) { + $TestStage.Check($SchemaNode, "$([LogicalVariable]$SubTests[$TestIndex].Name) occurred more than $($MaximumCount[$TestIndex]) times", $false) } - $Violates = "The following nodes are not accepted: $($Names -join ', ')" - if ($LastName) { - $LastName = if ($ObjectNode -is [PSListNode]) { [CommandColor]$LastName } - else { [StringColor][PSKeyExpression]::new($LastName, [PSSerialize]::MaxKeyLength) } - $Violates += " .. $LastName" + foreach ($NodeIndex in $Indices) { + if ($null -eq $Stages[$NodeIndex] -or $null -eq $Stages[$NodeIndex][$TestIndex]) { continue } + $Results = $Stages[$NodeIndex][$TestIndex].Results + if (-not $Results -or $Results.Count -eq 0) { continue } + $TestStage.AddResults($Results) + $TestStage.FailCount += $Results.Count } } } - -#EndRegion Optional nodes - - if ($Violates -or $Elaborate) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = if ($Violates) { $Violates } else { 'All the child nodes are valid'} - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { $RefInvalidNode.Value = $Output } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } + if (-not $TestStage.Passed -and $FailCount -eq $TestStage.FailCount) { + # Presumably concerns a negative requirement + # $Not = if ($TestStage.Passed) { ' not' } + $TestStage.Check($SchemaNode, "$ObjectNode is not accepted", $TestStage.Passed) + # if (-not $Best) { + # $TestStage.Check($SchemaNode, "$ObjectNode nodes did$Not pass", $TestStage.Passed) + # } + # elseif ($Best['TestPassed'].Count) { + # foreach ($TestIndex in $Best['TestPassed'].get_Keys()) { + # if ($Best['TestPassed'][$TestIndex]) { continue } + # $TestStage.Check($SchemaNode, "The requirement $([LogicalVariable]$SubTests[$TestIndex].Name) has$Not met", $false) + # } + # } + # else { + # for ($NodeIndex = 0; $NodeIndex -lt $SubNodes.Count; $NodeIndex++) { + # if ($UsedNodes.Contains($NodeIndex)) { continue } + # $TestStage.Check($SchemaNode, "$($SubNodes[$NodeIndex]) is$Not accepted", $false) + # } + # } } + #EndRegion SubTests } } process { $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Script:UniqueCollections = @{} - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestNode @TestParams - if ($ValidateOnly) { -not $Invalid } + $TestStage = [TestStage]::new($ObjectNode, $Elaborate, (-not $ValidateOnly), 0) + TestNode $TestStage $SchemaNode + if ($ValidateOnly) { $TestStage.Passed } } - diff --git a/Tests/Compare-ObjectGraph.Tests.ps1 b/Tests/Compare-ObjectGraph.Tests.ps1 index dd4ad1d..7a9a90e 100644 --- a/Tests/Compare-ObjectGraph.Tests.ps1 +++ b/Tests/Compare-ObjectGraph.Tests.ps1 @@ -2,8 +2,8 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Reference', Justification = 'False positive')] -param() +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Compare-ObjectGraph' { @@ -11,6 +11,12 @@ Describe 'Compare-ObjectGraph' { Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + $Reference = @{ Comment = 'Sample ObjectGraph' Data = @( @@ -35,8 +41,8 @@ Describe 'Compare-ObjectGraph' { Context 'Existence Check' { - It 'Help' { - Compare-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } @@ -97,6 +103,34 @@ Describe 'Compare-ObjectGraph' { $Result.Reference | Should -Be 'Sample ObjectGraph' } + It 'Single entry' { + $Object = @{ + Comment = 'Sample ObjectGraph' + Data = @( + @{ + Index = 1 + Name = 'One' + Comment = 'First item' + } + ) + } + $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False + $Result = $Object | Compare-ObjectGraph $Reference + $Result.Count | Should -Be 3 + $Result[0].Path | Should -Be 'Data' + $Result[0].Discrepancy | Should -Be 'Size' + $Result[0].InputObject | Should -Be 1 + $Result[0].Reference | Should -Be 3 + $Result[1].Path | Should -Be 'Data[1]' + $Result[1].Discrepancy | Should -Be 'Exists' + $Result[1].InputObject | Should -BeNullOrEmpty + $Result[1].Reference | Should -Not -BeNullOrEmpty + $Result[2].Path | Should -Be 'Data[2]' + $Result[2].Discrepancy | Should -Be 'Exists' + $Result[2].InputObject | Should -BeNullOrEmpty + $Result[2].Reference | Should -Not -BeNullOrEmpty + } + It 'Missing entry' { $Object = @{ Comment = 'Sample ObjectGraph' @@ -123,7 +157,7 @@ Describe 'Compare-ObjectGraph' { $Result[1].Path | Should -Be 'Data[2]' $Result[1].Discrepancy | Should -Be 'Exists' $Result[1].InputObject | Should -BeNullOrEmpty - $Result[1].Reference | Should -Be '[HashTable]' + $Result[1].Reference | Should -Not -BeNullOrEmpty } It 'Extra entry' { @@ -161,8 +195,8 @@ Describe 'Compare-ObjectGraph' { $Result[0].Reference | Should -Be 3 $Result[1].Path | Should -Be 'Data[3]' $Result[1].Discrepancy | Should -Be 'Exists' - $Result[1].InputObject | Should -Be '[HashTable]' - $Result[1].Reference | Should -Be $Null + $Result[1].InputObject | Should -Not -BeNullOrEmpty + $Result[1].Reference | Should -BeNullOrEmpty } It 'Different entry value' { @@ -460,31 +494,31 @@ Describe 'Compare-ObjectGraph' { $Result = $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False $Result = $Object | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } + $Lines = ($Result | Sort-Object Path | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } $Lines[00] | Should -be 'Path Discrepancy Reference InputObject' $Lines[01] | Should -be '---- ----------- --------- -----------' $Lines[02] | Should -be 'Data[0] Size 3 2' $Lines[03] | Should -be 'Data[0].Comment Exists True False' $Lines[04] | Should -be 'Data[1] Size 3 2' - $Lines[05] | Should -be 'Data[1].Name Value Two Three' + $Lines[05] | Should -be 'Data[1].Comment Exists True False' $Lines[06] | Should -be 'Data[1].Index Value 2 3' - $Lines[07] | Should -be 'Data[1].Comment Exists True False' + $Lines[07] | Should -be 'Data[1].Name Value Two Three' $Lines[08] | Should -be 'Data[2] Size 3 2' - $Lines[09] | Should -be 'Data[2].Name Value Three Two' + $Lines[09] | Should -be 'Data[2].Comment Exists True False' $Lines[10] | Should -be 'Data[2].Index Value 3 2' - $Lines[11] | Should -be 'Data[2].Comment Exists True False' + $Lines[11] | Should -be 'Data[2].Name Value Three Two' $Result = $Object | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False $Result = $Object | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } + $Lines = ($Result | Sort-Object Path | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' $Lines[1] | Should -be '---- ----------- --------- -----------' $Lines[2] | Should -be 'Data[0] Size 3 2' $Lines[3] | Should -be 'Data[0].Comment Exists True False' $Lines[4] | Should -be 'Data[1] Size 3 2' - $Lines[5] | Should -be 'Data[2].Comment Exists True False' + $Lines[5] | Should -be 'Data[1].Comment Exists True False' $Lines[6] | Should -be 'Data[2] Size 3 2' - $Lines[7] | Should -be 'Data[1].Comment Exists True False' + $Lines[7] | Should -be 'Data[2].Comment Exists True False' } } } diff --git a/Tests/ConvertFrom-Expression.Test.ps1 b/Tests/ConvertFrom-Expression.Tests.ps1 similarity index 82% rename from Tests/ConvertFrom-Expression.Test.ps1 rename to Tests/ConvertFrom-Expression.Tests.ps1 index 2b4e749..e1c78a7 100644 --- a/Tests/ConvertFrom-Expression.Test.ps1 +++ b/Tests/ConvertFrom-Expression.Tests.ps1 @@ -1,89 +1,94 @@ -#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"} - -using module ..\..\ObjectGraphTools - -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Expression', Justification = 'False positive')] -param() - -Describe 'ConvertFrom-Expression' { - - BeforeAll { - - # Set-StrictMode -Version Latest - } - - Context 'Existence Check' { - - It 'Help' { - ConvertFrom-Expression -? | Out-String -Stream | Should -Contain SYNOPSIS - } - } - - Context 'Constrained values' { - - BeforeAll { - - $Expression = @' -[PSCustomObject]@{ - first_name = 'John' - last_name = 'Smith' - is_alive = $True - birthday = [DateTime]'Monday, October 7, 1963 10:47:00 PM' - age = 27 - address = [PSCustomObject]@{ - street_address = '21 2nd Street' - city = 'New York' - state = 'NY' - postal_code = '10021-3100' - } - phone_numbers = @( - @{ number = '212 555-1234' }, - @{ number = '646 555-4567' } - ) - children = @('Catherine') - spouse = $Null -} -'@ - } - - It "Restricted mode" { - $Object = $Expression | ConvertFrom-Expression - $Object | Should -BeOfType HashTable - $Object.Keys | Sort-Object | Should -Be 'address', 'age', 'birthday', 'children', 'first_name', 'is_alive', 'last_name', 'phone_numbers', 'spouse' - } - - It "Constrained mode" { - $Object = $Expression | ConvertFrom-Expression -LanguageMode Constrained - $Object | ConvertTo-Expression -LanguageMode Constrained | Should -be @' -[PSCustomObject]@{ - first_name = 'John' - last_name = 'Smith' - is_alive = $True - birthday = [datetime]'1963-10-07T22:47:00.0000000' - age = 27 - address = [PSCustomObject]@{ - street_address = '21 2nd Street' - city = 'New York' - state = 'NY' - postal_code = '10021-3100' - } - phone_numbers = @( - @{ number = '212 555-1234' }, - @{ number = '646 555-4567' } - ) - children = @('Catherine') - spouse = $Null -} -'@ - } - } - - Context 'Issues' { - - It '#90 Add $PSCulture and $PSUICulture to the restricted language mode cmdlets and classes' { - '@{ Culture = $PSCulture }' | ConvertFrom-Expression | ConvertTo-Expression | Should -be "@{ Culture = '$PSCulture' }" - '@{ Culture = $PSUICulture }' | ConvertFrom-Expression | ConvertTo-Expression | Should -be "@{ Culture = '$PSUICulture' }" - } - } -} +#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"} + +using module ..\..\ObjectGraphTools + +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) + +Describe 'ConvertFrom-Expression' { + + BeforeAll { + + # Set-StrictMode -Version Latest + + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + } + + Context 'Existence Check' { + + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + } + } + + Context 'Constrained values' { + + BeforeAll { + + $Expression = @' +[PSCustomObject]@{ + first_name = 'John' + last_name = 'Smith' + is_alive = $True + birthday = [DateTime]'Monday, October 7, 1963 10:47:00 PM' + age = 27 + address = [PSCustomObject]@{ + street_address = '21 2nd Street' + city = 'New York' + state = 'NY' + postal_code = '10021-3100' + } + phone_numbers = @( + @{ number = '212 555-1234' }, + @{ number = '646 555-4567' } + ) + children = @('Catherine') + spouse = $Null +} +'@ + } + + It "Restricted mode" { + $Object = $Expression | ConvertFrom-Expression + $Object | Should -BeOfType HashTable + $Object.Keys | Sort-Object | Should -Be 'address', 'age', 'birthday', 'children', 'first_name', 'is_alive', 'last_name', 'phone_numbers', 'spouse' + } + + It "Constrained mode" { + $Object = $Expression | ConvertFrom-Expression -LanguageMode Constrained + $Object | ConvertTo-Expression -LanguageMode Constrained | Should -be @' +[PSCustomObject]@{ + first_name = 'John' + last_name = 'Smith' + is_alive = $True + birthday = [datetime]'1963-10-07T22:47:00.0000000' + age = 27 + address = [PSCustomObject]@{ + street_address = '21 2nd Street' + city = 'New York' + state = 'NY' + postal_code = '10021-3100' + } + phone_numbers = @( + @{ number = '212 555-1234' }, + @{ number = '646 555-4567' } + ) + children = @('Catherine') + spouse = $Null +} +'@ + } + } + + Context 'Issues' { + + It '#90 Add $PSCulture and $PSUICulture to the restricted language mode cmdlets and classes' { + '@{ Culture = $PSCulture }' | ConvertFrom-Expression | ConvertTo-Expression | Should -be "@{ Culture = '$PSCulture' }" + '@{ Culture = $PSUICulture }' | ConvertFrom-Expression | ConvertTo-Expression | Should -be "@{ Culture = '$PSUICulture' }" + } + } +} diff --git a/Tests/ConvertTo-Expression.Tests.ps1 b/Tests/ConvertTo-Expression.Tests.ps1 index 242d1b7..a3b9dac 100644 --- a/Tests/ConvertTo-Expression.Tests.ps1 +++ b/Tests/ConvertTo-Expression.Tests.ps1 @@ -3,15 +3,20 @@ using module ..\..\ObjectGraphTools [Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) -param() - -Describe 'Test-Object' { +Describe 'ConvertTo-Expression' { BeforeAll { Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + $Person = [PSCustomObject]@{ FirstName = 'John' LastName = 'Smith' @@ -1295,6 +1300,31 @@ Describe 'Test-Object' { $Person | Test-Object $Schema -ValidateOnly | Should -BeTrue $Person | Test-Object $Schema | Should -BeNullOrEmpty } + } + Context 'Github issues' { + + It "#97 RuntimeTypes don't properly ConvertTo-Expression" { + @{ '@Type' = [Int] } | ConvertTo-Expression | Should -be "@{ '@Type' = 'int' }" + @{ '@Type' = [Int] } | ConvertTo-Expression -LanguageMode Constrained | Should -be "@{ '@Type' = [Type]'int' }" + } + + It '#116 Use comma operator , for (embedded) empty arrays' { + ,@(,@()) | ConvertTo-Expression | Should -be @' +@( + ,@() +) +'@ + ,@(,@('Test')) | ConvertTo-Expression | Should -be @' +@( + ,@('Test') +) +'@ + ConvertTo-Expression @(,@()) -Expand 0 | Should -be '@(,@())' + ,@(,@()) | ConvertTo-Expression -Expand 0 | Should -be '@(,@())' + ,@(,@('Test')) | ConvertTo-Expression -Expand 0 | Should -be "@(,@('Test'))" + ,@(,[System.Collections.Generic.List[Object]]'Test') | ConvertTo-Expression -Expand 0 -LanguageMode Full | + Should -be "[Array]@(,[System.Collections.Generic.List[System.Object]]@([string]'Test'))" + } } } \ No newline at end of file diff --git a/Tests/Copy-ObjectGraph.Tests.ps1 b/Tests/Copy-ObjectGraph.Tests.ps1 index ddabc7b..23ebc5a 100644 --- a/Tests/Copy-ObjectGraph.Tests.ps1 +++ b/Tests/Copy-ObjectGraph.Tests.ps1 @@ -2,14 +2,21 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -param() +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Copy-ObjectGraph' { BeforeAll { + Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + $Object = @{ String = 'Hello World' Array = @( @@ -24,11 +31,12 @@ Describe 'Copy-ObjectGraph' { Context 'Existence Check' { - It 'Help' { - Copy-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } + Context 'Copy' { It 'Default' { diff --git a/Tests/Get-ChildNode.Tests.ps1 b/Tests/Get-ChildNode.Tests.ps1 index 361722f..60911f9 100644 --- a/Tests/Get-ChildNode.Tests.ps1 +++ b/Tests/Get-ChildNode.Tests.ps1 @@ -2,8 +2,8 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -param() +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Get-ChildNode' { @@ -11,6 +11,12 @@ Describe 'Get-ChildNode' { Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + $Object = @{ Comment = 'Sample ObjectGraph' Data = @( @@ -35,11 +41,12 @@ Describe 'Get-ChildNode' { Context 'Existence Check' { - It 'Help' { - Get-Node -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } + Context 'Basic selection' { it 'All child nodes' { @@ -198,4 +205,12 @@ Describe 'Get-ChildNode' { $Output.where{$_ -is [System.Management.Automation.WarningRecord]}.Message | Should -BeLike '*is a leaf node*' } } + + Context 'Issues' { + + It "#133 Get-ChildNode Name -Recurse -AtDepth doesn't work as expected" { + $Object = @{ Name = @{ Name = @{ Name = @{ Name = @{ Name = 1 } } } } } + $Object | Get-ChildNode Name -AtDepth 3 | Should -not -BeNullOrEmpty + } + } } \ No newline at end of file diff --git a/Tests/Get-Node.Tests.ps1 b/Tests/Get-Node.Tests.ps1 index ce20ae9..78d277e 100644 --- a/Tests/Get-Node.Tests.ps1 +++ b/Tests/Get-Node.Tests.ps1 @@ -2,10 +2,8 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'ObjectGraph', Justification = 'False positive')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Nodes', Justification = 'False positive')] -param() +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Get-Node' { @@ -13,6 +11,12 @@ Describe 'Get-Node' { Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + $Object = @{ Comment = 'Sample ObjectGraph' Data = @( @@ -37,8 +41,8 @@ Describe 'Get-Node' { Context 'Existence Check' { - It 'Help' { - Get-Node -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } @@ -138,6 +142,31 @@ Describe 'Get-Node' { } + Context 'Offspring' { + + BeforeAll { + $ObjectGraph = @{ Item = @{ Item = @{ Item = 123 } } } + } + + it 'Get first descendant nodes' { + $Node = $ObjectGraph | Get-Node ~Item + $Node.Depth | Should -Be 1 + $Node.Path | Should -Be Item + } + + it 'Get all offspring nodes' { + $Node = $ObjectGraph | Get-Node ~~Item + $Node.Count | Should -Be 3 + $Node.Depth | Should -Be 1, 2, 3 + } + + it 'Get last descendant leaf node' { + $Node = $ObjectGraph | Get-Node ~Item=* + $Node.Depth | Should -Be 3 + $Node.Path | Should -Be Item.Item.Item + } + } + Context 'Unique' { BeforeAll { @@ -153,6 +182,16 @@ Describe 'Get-Node' { } } + Context 'Escape' { + + it 'Wildcard' { + + @{ a = 'a' } | Get-Node a=* | Should -not -BeNullOrEmpty + @{ a = 'a' } | Get-Node a='`*' | Should -BeNullOrEmpty + @{ a = '*' } | Get-Node a='`*' | Should -not -BeNullOrEmpty + } + } + Context 'Change value' { BeforeAll { diff --git a/Tests/Import-ObjectGraph.Tests.ps1 b/Tests/Import-ObjectGraph.Tests.ps1 index 0e9b6fe..7a15be0 100644 --- a/Tests/Import-ObjectGraph.Tests.ps1 +++ b/Tests/Import-ObjectGraph.Tests.ps1 @@ -2,18 +2,20 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Expression', Justification = 'False positive')] -param() - -# $PesterPreference = [PesterConfiguration]::Default -# $PesterPreference.Should.ErrorAction = 'Stop' +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Import-ObjectGraph' { BeforeAll { - # Set-StrictMode -Version Latest + Set-StrictMode -Version Latest + + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } $PS1File = Join-Path $Env:Temp 'ObjectGraph.ps1' $PSD1File = Join-Path $Env:Temp 'ObjectGraph.psd1' @@ -46,8 +48,8 @@ Describe 'Import-ObjectGraph' { Context 'Existence Check' { - It 'Help' { - Import-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } diff --git a/Tests/LogicalFormula.Tests.ps1 b/Tests/LogicalFormula.Tests.ps1 new file mode 100644 index 0000000..bbb5bef --- /dev/null +++ b/Tests/LogicalFormula.Tests.ps1 @@ -0,0 +1,34 @@ +#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"} + +using module ..\..\ObjectGraphTools + +param() + +Describe 'LogicalFormula' { + + BeforeAll { + Set-StrictMode -Version Latest + } + + Context 'Existence Check' { + + It 'Loaded' { + [LogicalFormula]::new() -is [LogicalFormula] | Should -BeTrue + } + } + + Context 'Evaluate' { + $ScriptBlock = { + switch ($_) { + 'A' { $true } + 'B' { $false } + } + } + + $Formula = [LogicalFormula]::new("A or B") + $Formula.Evaluate($ScriptBlock) | Should -BeTrue + + $Formula = [LogicalFormula]::new("A and B") + $Formula.Evaluate($ScriptBlock) | Should -BeFalse + } +} \ No newline at end of file diff --git a/Tests/Merge-ObjectGraph.Tests.ps1 b/Tests/Merge-ObjectGraph.Tests.ps1 index 10fb58b..8157367 100644 --- a/Tests/Merge-ObjectGraph.Tests.ps1 +++ b/Tests/Merge-ObjectGraph.Tests.ps1 @@ -2,20 +2,30 @@ using module ..\..\ObjectGraphTools +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) + Describe 'Merge-ObjectGraph' { BeforeAll { Set-StrictMode -Version Latest - } + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + } + Context 'Existence Check' { - It 'Help' { - Merge-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } + Context 'Merge' { It 'Scalar Array' { diff --git a/Tests/PSNode.Tests.ps1 b/Tests/PSNode.Tests.ps1 index ca14240..6d07fb0 100644 --- a/Tests/PSNode.Tests.ps1 +++ b/Tests/PSNode.Tests.ps1 @@ -558,4 +558,12 @@ Describe 'PSNode' { ($HashTable | Get-Node).CaseMatters | Should -BeTrue } } + + # Context 'Github issues' { + + # it '#96 .Get_Value() return more than just the Value' { # See: https://stackoverflow.com/a/38212718/1701026 + # $a = @{ a = 'a', 'b' } | Get-Node + # $a.GetChildNode('a').Get_Value() | ConvertTo-Json -Compress | Should -be '["a","b"]' + # } + # } } diff --git a/Tests/Sort-ObjectGraph.Tests.ps1 b/Tests/Sort-ObjectGraph.Tests.ps1 index 55c9d8e..24a19b2 100644 --- a/Tests/Sort-ObjectGraph.Tests.ps1 +++ b/Tests/Sort-ObjectGraph.Tests.ps1 @@ -2,20 +2,26 @@ using module ..\..\ObjectGraphTools -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')] -param() +[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param([alias("Path")]$PrototypePath) Describe 'Sort-ObjectGraph' { BeforeAll { Set-StrictMode -Version Latest + + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + Mock $CommandName ([ScriptBlock]::Create($Content)) + } } Context 'Existence Check' { - It 'Help' { - Sort-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } diff --git a/Tests/Test-ObjectGraph.Tests.ps1 b/Tests/Test-ObjectGraph.Tests.ps1 index 64a496b..752cfee 100644 --- a/Tests/Test-ObjectGraph.Tests.ps1 +++ b/Tests/Test-ObjectGraph.Tests.ps1 @@ -3,15 +3,29 @@ using module ..\..\ObjectGraphTools [Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')] +param( + [alias("Path")]$PrototypePath +) -param() - -Describe 'Test-Object' { +Describe 'Test-ObjectGraph' { BeforeAll { Set-StrictMode -Version Latest + if ($PrototypePath) { + $Content = Get-Content -Raw -LiteralPath $PrototypePath + $CommandName = [io.path]::GetFileNameWithoutExtension($PSCommandPath) -replace '\.Tests$' + # New-Item -Path Function:$CommandName -Value $Content -Force + Mock $CommandName ([ScriptBlock]::Create($Content)) + } + + function Select-Issue { + param( [Parameter(Mandatory = $true, ValueFromPipeLine = $true)][PSCustomObject]$InputObject ) + process { $InputObject.Issue -replace '\x1b\[[0-9;]*m' } + } + + $Person = [PSCustomObject]@{ FirstName = 'John' LastName = 'Smith' @@ -24,70 +38,70 @@ Describe 'Test-Object' { State = 'NY' PostalCode = '10021-3100' } - Phone = @{ + Phone = @{ Home = '212 555-1234' Mobile = '212 555-2345' Work = '212 555-3456', '212 555-3456', '646 555-4567' } - Children = @('Dennis', 'Stefan') - Spouse = $Null + Children = @('Dennis', 'Stefan') + Spouse = $Null } } Context 'Existence Check' { - It 'Help' { - Test-Object -? | Out-String -Stream | Should -Contain SYNOPSIS + It 'Help' -Skip:$($null -ne $PrototypePath) { + Test-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS } } Context 'Type (as string)' { It 'Report' { - $True | Test-Object @{ '@Type' = 'Bool' } | Should -BeNullOrEmpty - $Report = 123 | Test-ObjectGraph @{ '@Type' = 'Bool' } -Elaborate + $True | Test-ObjectGraph @{ '@Type' = 'Bool' } | Should -BeNullOrEmpty + $Report = 123 | Test-ObjectGraph @{ '@Type' = 'Bool' } $Report.ObjectNode.Value | Should -Be 123 $Report.Valid | Should -Be $False $Report.Issue | Should -Not -BeNullOrEmpty } It 'Bool' { - $True | Test-Object @{ '@Type' = 'Bool' } -ValidateOnly | Should -BeTrue - 'True' | Test-Object @{ '@Type' = 'Bool' } -ValidateOnly | Should -BeFalse + $True | Test-ObjectGraph @{ '@Type' = 'Bool' } -ValidateOnly | Should -BeTrue + 'True' | Test-ObjectGraph @{ '@Type' = 'Bool' } -ValidateOnly | Should -BeFalse } It 'Int' { - 123 | Test-Object @{ '@Type' = 'Int' } -ValidateOnly | Should -BeTrue - '123' | Test-Object @{ '@Type' = 'Int' } -ValidateOnly | Should -BeFalse + 123 | Test-ObjectGraph @{ '@Type' = 'Int' } -ValidateOnly | Should -BeTrue + '123' | Test-ObjectGraph @{ '@Type' = 'Int' } -ValidateOnly | Should -BeFalse } It 'String' { - 'True' | Test-Object @{ '@Type' = 'String' } -ValidateOnly | Should -BeTrue - '123' | Test-Object @{ '@Type' = 'String' } -ValidateOnly | Should -BeTrue - 123 | Test-Object @{ '@Type' = 'String' } -ValidateOnly | Should -BeFalse + 'True' | Test-ObjectGraph @{ '@Type' = 'String' } -ValidateOnly | Should -BeTrue + '123' | Test-ObjectGraph @{ '@Type' = 'String' } -ValidateOnly | Should -BeTrue + 123 | Test-ObjectGraph @{ '@Type' = 'String' } -ValidateOnly | Should -BeFalse } It 'Array' { - ,@(1,2) | Test-Object @{ '@Type' = 'Array'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@(1) | Test-Object @{ '@Type' = 'Array'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@() | Test-Object @{ '@Type' = 'Array' } -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @{ '@Type' = 'Array'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object @{ '@Type' = 'Array'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + , @(1, 2) | Test-ObjectGraph @{ '@Type' = 'Array'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @(1) | Test-ObjectGraph @{ '@Type' = 'Array'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @() | Test-ObjectGraph @{ '@Type' = 'Array' } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@Type' = 'Array'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'Array'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } It 'HashTable' { - @{ a = 1 } | Test-Object @{ '@Type' = 'HashTable'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - @{} | Test-Object @{ '@Type' = 'HashTable' } -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @{ '@Type' = 'HashTable'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - ,@(1, 2) | Test-Object @{ '@Type' = 'HashTable'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'HashTable'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + @{} | Test-ObjectGraph @{ '@Type' = 'HashTable' } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@Type' = 'HashTable'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + , @(1, 2) | Test-ObjectGraph @{ '@Type' = 'HashTable'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } } Context 'Type (as type)' { It 'Report' { - $True | Test-Object @{ '@Type' = [Bool] } | Should -BeNullOrEmpty - 123 | Test-Object @{ '@Type' = [Bool] } -Elaborate | ForEach-Object { + $True | Test-ObjectGraph @{ '@Type' = [Bool] } | Should -BeNullOrEmpty + 123 | Test-ObjectGraph @{ '@Type' = [Bool] } -Elaborate | ForEach-Object { $_.ObjectNode.Value | Should -Be 123 $_.Valid | Should -Be $False $_.Issue | Should -Not -BeNullOrEmpty @@ -95,264 +109,364 @@ Describe 'Test-Object' { } It 'Bool' { - $True | Test-Object @{ '@Type' = [Bool] } -ValidateOnly | Should -BeTrue - 'True' | Test-Object @{ '@Type' = [Bool] } -ValidateOnly | Should -BeFalse + $True | Test-ObjectGraph @{ '@Type' = [Bool] } -ValidateOnly | Should -BeTrue + 'True' | Test-ObjectGraph @{ '@Type' = [Bool] } -ValidateOnly | Should -BeFalse } It 'Int' { - 123 | Test-Object @{ '@Type' = [Int] } -ValidateOnly | Should -BeTrue - '123' | Test-Object @{ '@Type' = [Int] } -ValidateOnly | Should -BeFalse + 123 | Test-ObjectGraph @{ '@Type' = [Int] } -ValidateOnly | Should -BeTrue + '123' | Test-ObjectGraph @{ '@Type' = [Int] } -ValidateOnly | Should -BeFalse } It 'String' { - 'True' | Test-Object @{ '@Type' = [String] } -ValidateOnly | Should -BeTrue - '123' | Test-Object @{ '@Type' = [String] } -ValidateOnly | Should -BeTrue - 123 | Test-Object @{ '@Type' = [String] } -ValidateOnly | Should -BeFalse + 'True' | Test-ObjectGraph @{ '@Type' = [String] } -ValidateOnly | Should -BeTrue + '123' | Test-ObjectGraph @{ '@Type' = [String] } -ValidateOnly | Should -BeTrue + 123 | Test-ObjectGraph @{ '@Type' = [String] } -ValidateOnly | Should -BeFalse } It 'Array' { - ,@(1,2) | Test-Object @{ '@Type' = [Array]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@(1) | Test-Object @{ '@Type' = [Array]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@() | Test-Object @{ '@Type' = [Array] } -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @{ '@Type' = [Array]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object @{ '@Type' = [Array]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + , @(1, 2) | Test-ObjectGraph @{ '@Type' = [Array]; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @(1) | Test-ObjectGraph @{ '@Type' = [Array]; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @() | Test-ObjectGraph @{ '@Type' = [Array] } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@Type' = [Array]; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = [Array]; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } It 'HashTable' { - @{ a = 1 } | Test-Object @{ '@Type' = [HashTable]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - @{} | Test-Object @{ '@Type' = [HashTable] } -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @{ '@Type' = [HashTable]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - ,@(1, 2) | Test-Object @{ '@Type' = [HashTable]; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = [HashTable]; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + @{} | Test-ObjectGraph @{ '@Type' = [HashTable] } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@Type' = [HashTable]; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + , @(1, 2) | Test-ObjectGraph @{ '@Type' = [HashTable]; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } } Context 'Not Type (as string)' { It 'Bool' { - 'True' | Test-Object @{ '@NotType' = 'Bool' } -ValidateOnly | Should -BeTrue - $True | Test-Object @{ '@NotType' = 'Bool' } -ValidateOnly | Should -BeFalse + 'True' | Test-ObjectGraph @{ '@NotType' = 'Bool' } -ValidateOnly | Should -BeTrue + $True | Test-ObjectGraph @{ '@NotType' = 'Bool' } -ValidateOnly | Should -BeFalse } } Context 'Not Type (as type)' { It 'Not Bool' { - 'True' | Test-Object @{ '@NotType' = [Bool] } -ValidateOnly | Should -BeTrue - $True | Test-Object @{ '@NotType' = [Bool] } -ValidateOnly | Should -BeFalse + 'True' | Test-ObjectGraph @{ '@NotType' = [Bool] } -ValidateOnly | Should -BeTrue + $True | Test-ObjectGraph @{ '@NotType' = [Bool] } -ValidateOnly | Should -BeFalse } } - Context 'Multiple types' { It 'Any of type' { - '123' | Test-Object @{ '@Type' = [Int], [String] } -ValidateOnly | Should -BeTrue - $true | Test-Object @{ '@Type' = [Int], [String] } -ValidateOnly | Should -BeFalse + '123' | Test-ObjectGraph @{ '@Type' = [Int], [String] } -ValidateOnly | Should -BeTrue + $true | Test-ObjectGraph @{ '@Type' = [Int], [String] } -ValidateOnly | Should -BeFalse } It 'None of type' { - '123' | Test-Object @{ '@NotType' = [Int], [String] } -ValidateOnly | Should -BeFalse - $true | Test-Object @{ '@NotType' = [Int], [String] } -ValidateOnly | Should -BeTrue + '123' | Test-ObjectGraph @{ '@NotType' = [Int], [String] } -ValidateOnly | Should -BeFalse + $true | Test-ObjectGraph @{ '@NotType' = [Int], [String] } -ValidateOnly | Should -BeTrue } } Context 'No type' { It '$Null' { - @{ Test = '123' } | Test-Object @{ Test = @{ '@Type' = [Int], [String] } } -ValidateOnly | Should -BeTrue - @{ Test = $Null } | Test-Object @{ Test = @{ '@Type' = [Int], [String] } } -ValidateOnly | Should -BeFalse - @{ Test = $Null } | Test-Object @{ Test = @{ '@Type' = [Int], [String], [Void] } } -ValidateOnly | Should -BeTrue - @{ Test = $Null } | Test-Object @{ Test = @{ '@Type' = [Int], [String], 'Null' } } -ValidateOnly | Should -BeTrue - @{ Test = $Null } | Test-Object @{ Test = @{ '@Type' = [Int], [String], $Null } } -ValidateOnly | Should -BeTrue - @{ Test = '123' } | Test-Object @{ Test = @{ '@Type' = [Int], [Void] } } -ValidateOnly | Should -BeFalse + @{ Test = '123' } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [String] } } -ValidateOnly | Should -BeTrue + @{ Test = $Null } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [String] } } -ValidateOnly | Should -BeFalse + @{ Test = $Null } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [String], [Void] } } -ValidateOnly | Should -BeTrue + @{ Test = $Null } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [String], 'Null' } } -ValidateOnly | Should -BeTrue + @{ Test = $Null } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [String], $Null } } -ValidateOnly | Should -BeTrue + @{ Test = '123' } | Test-ObjectGraph @{ Test = @{ '@Type' = [Int], [Void] } } -ValidateOnly | Should -BeFalse } } Context 'PSNode Type' { It 'Value' { - 'String' | Test-Object @{ '@Type' = 'PSNode' } -ValidateOnly | Should -BeTrue - 'String' | Test-Object @{ '@Type' = 'PSLeafNode' } -ValidateOnly | Should -BeTrue - 'String' | Test-Object @{ '@Type' = 'PSCollectionNode' } -ValidateOnly | Should -BeFalse - 'String' | Test-Object @{ '@Type' = 'PSListNode' } -ValidateOnly | Should -BeFalse - 'String' | Test-Object @{ '@Type' = 'PSMapNode' } -ValidateOnly | Should -BeFalse - 'String' | Test-Object @{ '@Type' = 'PSObjectNode' } -ValidateOnly | Should -BeFalse + 'String' | Test-ObjectGraph @{ '@Type' = 'PSNode' } -ValidateOnly | Should -BeTrue + 'String' | Test-ObjectGraph @{ '@Type' = 'PSLeafNode' } -ValidateOnly | Should -BeTrue + 'String' | Test-ObjectGraph @{ '@Type' = 'PSCollectionNode' } -ValidateOnly | Should -BeFalse + 'String' | Test-ObjectGraph @{ '@Type' = 'PSListNode' } -ValidateOnly | Should -BeFalse + 'String' | Test-ObjectGraph @{ '@Type' = 'PSMapNode' } -ValidateOnly | Should -BeFalse + 'String' | Test-ObjectGraph @{ '@Type' = 'PSObjectNode' } -ValidateOnly | Should -BeFalse } It 'Array' { - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSLeafNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSCollectionNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSListNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSMapNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - ,@(1,2,3) | Test-Object @{ '@Type' = 'PSObjectNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSLeafNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSCollectionNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSListNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSMapNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + , @(1, 2, 3) | Test-ObjectGraph @{ '@Type' = 'PSObjectNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } It 'Dictionary' { - @{ a = 1 } | Test-Object @{ '@Type' = 'PSNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object @{ '@Type' = 'PSLeafNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object @{ '@Type' = 'PSCollectionNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object @{ '@Type' = 'PSListNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object @{ '@Type' = 'PSMapNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object @{ '@Type' = 'PSObjectNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSLeafNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSCollectionNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSListNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSMapNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph @{ '@Type' = 'PSObjectNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse } It 'Object' { - $Person | Test-Object @{ '@Type' = 'PSNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - $Person | Test-Object @{ '@Type' = 'PSLeafNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - $Person | Test-Object @{ '@Type' = 'PSCollectionNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - $Person | Test-Object @{ '@Type' = 'PSListNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeFalse - $Person | Test-Object @{ '@Type' = 'PSMapNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - $Person | Test-Object @{ '@Type' = 'PSObjectNode'; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ '@Type' = 'PSNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ '@Type' = 'PSLeafNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + $Person | Test-ObjectGraph @{ '@Type' = 'PSCollectionNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ '@Type' = 'PSListNode'; '@AnyName' = @() } -ValidateOnly | Should -BeFalse + $Person | Test-ObjectGraph @{ '@Type' = 'PSMapNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ '@Type' = 'PSObjectNode'; '@AnyName' = @() } -ValidateOnly | Should -BeTrue } } - Context 'Natural list ' { - BeforeAll { - $Schema = @{ Word = @{ '@Match' = '^\w+$' } } - } - it 'No list' { @{} | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it '$Null' { @{ Word = $null } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Test' { @{ Word = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Empty' { @{ Word = @() } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Single' { @{ Word = @('a') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Multiple' { @{ Word = @('a', 'b') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'No match a b' { @{ Word = @('a b') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a, b c' { @{ Word = @('a', 'b c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a b, c' { @{ Word = @('a b', 'c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Dictionary' { @{ Word = @{ a = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'IsWord item' { @{ Word = @{ IsWord = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeTrue } + Context 'Map (name based) test' { + + BeforeAll { $Schema = @{ Foo = @{ '@Like' = 'Bar' } } } + + It '@Like' { + @{} | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @($null) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @()) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@{}) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @('Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(, @('Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @('Bar') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @('Baz') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Bar', 'Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(@('Bar', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Baz', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Bar', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Baz', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Foo' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Bar'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Baz = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar'; Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + } } - Context 'Compulsory list with unnamed items' { - BeforeAll { - $Schema = @{ Word = @(@{ '@Match' = '^\w+$' }) } - } - it 'No list' { @{} | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it '$Null' { @{ Word = $null } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Test' { @{ Word = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Empty' { @{ Word = @() } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Single' { @{ Word = @('a') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Multiple' { @{ Word = @('a', 'b') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'No match a b' { @{ Word = @('a b') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a, b c' { @{ Word = @('a', 'b c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a b, c' { @{ Word = @('a b', 'c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Dictionary' { @{ Word = @{ a = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'IsWord item' { @{ Word = @{ IsWord = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } + Context 'List (value based) test' { + + BeforeAll { $Schema = @(@{ '@Like' = 'Bar' }) } + + It '@Like' { + @{} | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @($null) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @()) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@{}) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @('Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(, @('Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @('Bar') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @('Baz') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Bar', 'Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(@('Bar', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Baz', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Bar', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Baz', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = @('Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = 'Foo' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = @('Bar', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Foo', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Bar'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Baz = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar'; Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + } } - Context 'Compulsory list with named items' { - BeforeAll { - $Schema = @{ Word = @{ '@Type' = [PSListNode]; IsWord = @{ '@Match' = '^\w+$' } } } - } - it 'No list' { @{} | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it '$Null' { @{ Word = $null } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Test' { @{ Word = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Empty' { @{ Word = @() } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Single' { @{ Word = @('a') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Multiple' { @{ Word = @('a', 'b') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'No match a b' { @{ Word = @('a b') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a, b c' { @{ Word = @('a', 'b c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a b, c' { @{ Word = @('a b', 'c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Dictionary' { @{ Word = @{ a = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'IsWord item' { @{ Word = @{ IsWord = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } + Context 'Map (name based) test allowing addition names' { + + BeforeAll { $Schema = @{ Foo = @{ '@Like' = 'Bar' }; '@AnyName' = @() } } + + It '@Like' { + @{} | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @($null) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @()) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@{}) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @('Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(, @('Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @('Bar') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @('Baz') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Bar', 'Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(@('Bar', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(@('Baz', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Bar', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Baz', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Foo' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Foo', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Bar'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Baz = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar'; Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + } } - Context 'Forced list with named items' { - BeforeAll { - $Schema = @{ Word = @{ '@Type' = [PSListNode]; '@Required' = $true; IsWord = @{ '@Match' = '^\w+$' } } } - } - it 'No list' { @{} | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it '$Null' { @{ Word = $null } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Test' { @{ Word = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Empty' { @{ Word = @() } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Single' { @{ Word = @('a') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'Multiple' { @{ Word = @('a', 'b') } | Test-Object $Schema -ValidateOnly | Should -BeTrue } - it 'No match a b' { @{ Word = @('a b') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a, b c' { @{ Word = @('a', 'b c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'No match a b, c' { @{ Word = @('a b', 'c') } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'Dictionary' { @{ Word = @{ a = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } - it 'IsWord item' { @{ Word = @{ IsWord = 'b' } } | Test-Object $Schema -ValidateOnly | Should -BeFalse } + Context 'List (value based) test allowing additional values' { + + BeforeAll { $Schema = @(@{ '@Like' = 'Bar'; '@AnyName' = @() }) } + + It '@Like' { + @{} | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @($null) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @()) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@{}) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(, @('Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(, @('Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @('Bar') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @('Baz') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Bar', 'Bar')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @(@('Bar', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @(@('Baz', 'Baz')) | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Foo = @('Bar', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = @('Baz', 'Baz') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = $null } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @() } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @{} } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = @('Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = 'Foo' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Bar', 'Bar') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Baz = @('Bar', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Baz = @('Foo', 'Foo') } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Bar'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Foo = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Bar = 'Foo'; Baz = 'Baz' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Foo = 'Bar'; Baz = 'Bar' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + } } Context 'Multiple integer Limits' { It 'Maximum int' { - ,@(17, 18, 19) | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue - ,@(40, 41, 42) | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue - ,@(17, 42, 99) | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeFalse + , @(17, 18, 19) | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue + , @(40, 41, 42) | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue + , @(17, 42, 99) | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeFalse } It 'Exclusive maximum int' { - ,@(17, 18, 19) | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeTrue - ,@(40, 41, 42) | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse - ,@(17, 42, 99) | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse + , @(17, 18, 19) | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeTrue + , @(40, 41, 42) | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse + , @(17, 42, 99) | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse } It 'Minimum int' { - ,@(97, 98, 99) | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue - ,@(42, 43, 44) | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue - ,@(17, 42, 99) | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeFalse + , @(97, 98, 99) | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue + , @(42, 43, 44) | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue + , @(17, 42, 99) | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeFalse } It 'Exclusive minimum int' { - ,@(97, 98, 99) | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeTrue - ,@(42, 43, 44) | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse - ,@(17, 42, 99) | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse + , @(97, 98, 99) | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeTrue + , @(42, 43, 44) | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse + , @(17, 42, 99) | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse } } Context 'String limits' { It 'Maximum string' { - 'Alpha' | Test-Object @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Beta' | Test-Object @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Gamma' | Test-Object @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Beta' | Test-ObjectGraph @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Gamma' | Test-ObjectGraph @{ '@Maximum' = 'Beta' } -ValidateOnly | Should -BeFalse } It 'Exclusive maximum string' { - 'Alpha' | Test-Object @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Beta' | Test-Object @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeFalse - 'Gamma' | Test-Object @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Beta' | Test-ObjectGraph @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Gamma' | Test-ObjectGraph @{ '@ExclusiveMaximum' = 'Beta' } -ValidateOnly | Should -BeFalse } It 'Minimum string' { - 'Gamma' | Test-Object @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Beta' | Test-Object @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Alpha' | Test-Object @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Gamma' | Test-ObjectGraph @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Beta' | Test-ObjectGraph @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Alpha' | Test-ObjectGraph @{ '@Minimum' = 'Beta' } -ValidateOnly | Should -BeFalse } It 'Exclusive minimum string' { - 'Gamma' | Test-Object @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeTrue - 'Beta' | Test-Object @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeFalse - 'Alpha' | Test-Object @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Gamma' | Test-ObjectGraph @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeTrue + 'Beta' | Test-ObjectGraph @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@ExclusiveMinimum' = 'Beta' } -ValidateOnly | Should -BeFalse } } Context 'Integer Limits' { It 'Maximum int' { - 17 | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue - 42 | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue - 99 | Test-Object @{ '@Maximum' = 42 } -ValidateOnly | Should -BeFalse + 17 | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue + 42 | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeTrue + 99 | Test-ObjectGraph @{ '@Maximum' = 42 } -ValidateOnly | Should -BeFalse } It 'Exclusive maximum int' { - 17 | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeTrue - 42 | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse - 99 | Test-Object @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse + 17 | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeTrue + 42 | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse + 99 | Test-ObjectGraph @{ '@ExclusiveMaximum' = 42 } -ValidateOnly | Should -BeFalse } It 'Minimum int' { - 99 | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue - 42 | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue - 17 | Test-Object @{ '@Minimum' = 42 } -ValidateOnly | Should -BeFalse + 99 | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue + 42 | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeTrue + 17 | Test-ObjectGraph @{ '@Minimum' = 42 } -ValidateOnly | Should -BeFalse } It 'Exclusive minimum int' { - 99 | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeTrue - 42 | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse - 17 | Test-Object @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse + 99 | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeTrue + 42 | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse + 17 | Test-ObjectGraph @{ '@ExclusiveMinimum' = 42 } -ValidateOnly | Should -BeFalse } } @@ -360,127 +474,127 @@ Describe 'Test-Object' { Context 'Case sensitive string limits' { It 'Maximum string' { - 'alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Maximum' = 'Alpha' } -ValidateOnly | Should -BeTrue - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Maximum' = 'Alpha' } -ValidateOnly | Should -BeTrue - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Maximum' = 'alpha' } -ValidateOnly | Should -BeFalse + 'alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Maximum' = 'Alpha' } -ValidateOnly | Should -BeTrue + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Maximum' = 'Alpha' } -ValidateOnly | Should -BeTrue + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Maximum' = 'alpha' } -ValidateOnly | Should -BeFalse } It 'Maximum exclusive string' { - 'alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'Alpha' } -ValidateOnly | Should -BeTrue - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'Alpha' } -ValidateOnly | Should -BeFalse - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'alpha' } -ValidateOnly | Should -BeFalse + 'alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'Alpha' } -ValidateOnly | Should -BeTrue + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'Alpha' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMaximum' = 'alpha' } -ValidateOnly | Should -BeFalse } It 'Minimum string' { - 'alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Minimum' = 'Alpha' } -ValidateOnly | Should -BeFalse - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Minimum' = 'Alpha' } -ValidateOnly | Should -BeTrue - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@Minimum' = 'alpha' } -ValidateOnly | Should -BeTrue + 'alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Minimum' = 'Alpha' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Minimum' = 'Alpha' } -ValidateOnly | Should -BeTrue + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Minimum' = 'alpha' } -ValidateOnly | Should -BeTrue } It 'Minimum exclusive string' { - 'alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'Alpha' } -ValidateOnly | Should -BeFalse - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'Alpha' } -ValidateOnly | Should -BeFalse - 'Alpha' | Test-Object @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'alpha' } -ValidateOnly | Should -BeTrue + 'alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'Alpha' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'Alpha' } -ValidateOnly | Should -BeFalse + 'Alpha' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@ExclusiveMinimum' = 'alpha' } -ValidateOnly | Should -BeTrue } } Context 'String length limits' { It 'Minimum length' { - 'abc' | Test-Object @{ '@MinimumLength' = 4 } -ValidateOnly | Should -BeFalse - 'abcd' | Test-Object @{ '@MinimumLength' = 4 } -ValidateOnly | Should -BeTrue + 'abc' | Test-ObjectGraph @{ '@MinimumLength' = 4 } -ValidateOnly | Should -BeFalse + 'abcd' | Test-ObjectGraph @{ '@MinimumLength' = 4 } -ValidateOnly | Should -BeTrue } It 'Length' { - 'ab' | Test-Object @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse - 'abc' | Test-Object @{ '@Length' = 3 } -ValidateOnly | Should -BeTrue - 'abcd' | Test-Object @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse + 'ab' | Test-ObjectGraph @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse + 'abc' | Test-ObjectGraph @{ '@Length' = 3 } -ValidateOnly | Should -BeTrue + 'abcd' | Test-ObjectGraph @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse } It 'Maximum length' { - 'abc' | Test-Object @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeTrue - 'abcd' | Test-Object @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeFalse + 'abc' | Test-ObjectGraph @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeTrue + 'abcd' | Test-ObjectGraph @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeFalse } It 'Multiple values length' { - ,@('12', '345', '6789') | Test-Object @{ '@MinimumLength' = 2 } -ValidateOnly | Should -BeTrue - ,@('12', '345', '6789') | Test-Object @{ '@MinimumLength' = 3 } -ValidateOnly | Should -BeFalse - ,@('123', '456', '789') | Test-Object @{ '@Length' = 3 } -ValidateOnly | Should -BeTrue - ,@('12', '345', '6789') | Test-Object @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse - ,@('12', '345', '6789') | Test-Object @{ '@MaximumLength' = 4 } -ValidateOnly | Should -BeTrue - ,@('12', '345', '6789') | Test-Object @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeFalse + , @('12', '345', '6789') | Test-ObjectGraph @{ '@MinimumLength' = 2 } -ValidateOnly | Should -BeTrue + , @('12', '345', '6789') | Test-ObjectGraph @{ '@MinimumLength' = 3 } -ValidateOnly | Should -BeFalse + , @('123', '456', '789') | Test-ObjectGraph @{ '@Length' = 3 } -ValidateOnly | Should -BeTrue + , @('12', '345', '6789') | Test-ObjectGraph @{ '@Length' = 3 } -ValidateOnly | Should -BeFalse + , @('12', '345', '6789') | Test-ObjectGraph @{ '@MaximumLength' = 4 } -ValidateOnly | Should -BeTrue + , @('12', '345', '6789') | Test-ObjectGraph @{ '@MaximumLength' = 3 } -ValidateOnly | Should -BeFalse } } Context 'Patterns' { It 'Like' { - 'test' | Test-Object @{ '@Like' = 'T*t' } -ValidateOnly | Should -BeTrue - 'test' | Test-Object @{ '@Like' = 'T?t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@Like' = 'T*t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@Like' = 'T?t' } -ValidateOnly | Should -BeFalse } It 'Not like' { - 'test' | Test-Object @{ '@NotLike' = 'T*t' } -ValidateOnly | Should -BeFalse - 'test' | Test-Object @{ '@NotLike' = 'T?t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@NotLike' = 'T*t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@NotLike' = 'T?t' } -ValidateOnly | Should -BeTrue } It 'Match' { - 'test' | Test-Object @{ '@Match' = 'T.*t' } -ValidateOnly | Should -BeTrue - 'test' | Test-Object @{ '@Match' = 'T.t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@Match' = 'T.*t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@Match' = 'T.t' } -ValidateOnly | Should -BeFalse } It 'Not match' { - 'test' | Test-Object @{ '@NotMatch' = 'T.*t' } -ValidateOnly | Should -BeFalse - 'test' | Test-Object @{ '@NotMatch' = 'T.t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@NotMatch' = 'T.*t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@NotMatch' = 'T.t' } -ValidateOnly | Should -BeTrue } } Context 'Case sensitive patterns' { It 'Like' { - 'Test' | Test-Object @{ '@CaseSensitive' = $true; '@Like' = 'T*t' } -ValidateOnly | Should -BeTrue - 'test' | Test-Object @{ '@CaseSensitive' = $true; '@Like' = 'T*t' } -ValidateOnly | Should -BeFalse + 'Test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Like' = 'T*t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Like' = 'T*t' } -ValidateOnly | Should -BeFalse } It 'Not like' { - 'Test' | Test-Object @{ '@CaseSensitive' = $true; '@notLike' = 'T*t' } -ValidateOnly | Should -BeFalse - 'test' | Test-Object @{ '@CaseSensitive' = $true; '@notLike' = 'T*t' } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@notLike' = 'T*t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@notLike' = 'T*t' } -ValidateOnly | Should -BeTrue } It 'Match' { - 'Test' | Test-Object @{ '@CaseSensitive' = $true; '@Match' = 'T..t' } -ValidateOnly | Should -BeTrue - 'test' | Test-Object @{ '@CaseSensitive' = $true; '@Match' = 'T..t' } -ValidateOnly | Should -BeFalse + 'Test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Match' = 'T..t' } -ValidateOnly | Should -BeTrue + 'test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@Match' = 'T..t' } -ValidateOnly | Should -BeFalse } It 'Not match' { - 'Test' | Test-Object @{ '@CaseSensitive' = $true; '@notMatch' = 'T..t' } -ValidateOnly | Should -BeFalse - 'test' | Test-Object @{ '@CaseSensitive' = $true; '@notMatch' = 'T..t' } -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@notMatch' = 'T..t' } -ValidateOnly | Should -BeFalse + 'test' | Test-ObjectGraph @{ '@CaseSensitive' = $true; '@notMatch' = 'T..t' } -ValidateOnly | Should -BeTrue } } Context 'Multiple patterns' { It 'Like' { - 'Two' | Test-Object @{ '@Like' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue - 'Four' | Test-Object @{ '@Like' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse + 'Two' | Test-ObjectGraph @{ '@Like' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue + 'Four' | Test-ObjectGraph @{ '@Like' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse } It 'Not like' { - 'Two' | Test-Object @{ '@NotLike' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse - 'Four' | Test-Object @{ '@NotLike' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue + 'Two' | Test-ObjectGraph @{ '@NotLike' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse + 'Four' | Test-ObjectGraph @{ '@NotLike' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue } It 'Match' { - 'Two' | Test-Object @{ '@Match' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue - 'Four' | Test-Object @{ '@Match' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse + 'Two' | Test-ObjectGraph @{ '@Match' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue + 'Four' | Test-ObjectGraph @{ '@Match' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse } It 'Not match' { - 'Two' | Test-Object @{ '@NotMatch' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse - 'Four' | Test-Object @{ '@NotMatch' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue + 'Two' | Test-ObjectGraph @{ '@NotMatch' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeFalse + 'Four' | Test-ObjectGraph @{ '@NotMatch' = 'One', 'Two', 'Three' } -ValidateOnly | Should -BeTrue } } @@ -488,125 +602,125 @@ Describe 'Test-Object' { Context 'No (child) Node' { It "[V] Leaf node" { - 'Test' | Test-Object @{} -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @{} | Should -BeNullOrEmpty + 'Test' | Test-ObjectGraph @{} -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @{} | Should -BeNullOrEmpty } It "[V] Empty list node" { - ,@() | Test-Object @{} -ValidateOnly | Should -BeTrue - ,@() | Test-Object @{} | Should -BeNullOrEmpty + , @() | Test-ObjectGraph @{} -ValidateOnly | Should -BeTrue + , @() | Test-ObjectGraph @{} | Should -BeNullOrEmpty } - It "[X] Simple list node" { - ,@('Test') | Test-Object @{} -ValidateOnly | Should -BeFalse - $Result = ,@('Test') | Test-Object @{} + It "[X] Single list node" { + , @('Test') | Test-ObjectGraph @{} -ValidateOnly | Should -BeFalse + $Result = , @('Test') | Test-ObjectGraph @{} $Result | Should -BeOfType PSCustomObject $Result.Valid | Should -BeFalse $Result.ObjectNode.Path | Should -BeNullOrEmpty - $Result.Issue | Should -BeLike '*not accepted*0*' + $Result | Select-Issue | Should -Be "@('Test') is not accepted" } It "[X] Simple list node" { - ,@('a', 'b') | Test-Object @{} -ValidateOnly | Should -BeFalse - ,@('a', 'b') | Test-Object @{}| Should -not -BeNullOrEmpty + , @('a', 'b') | Test-ObjectGraph @{} -ValidateOnly | Should -BeFalse + , @('a', 'b') | Test-ObjectGraph @{} | Should -Not -BeNullOrEmpty } It "[V] Empty map node" { - @{} | Test-Object @{} -ValidateOnly | Should -BeTrue - @{} | Test-Object @{} | Should -BeNullOrEmpty + @{} | Test-ObjectGraph @{} -ValidateOnly | Should -BeTrue + @{} | Test-ObjectGraph @{} | Should -BeNullOrEmpty } It "[X] Simple map node" { - @{ a = 1 } | Test-Object @{} -ValidateOnly | Should -BeFalse - $Result = @{ a = 1 } | Test-Object @{} + @{ a = 1 } | Test-ObjectGraph @{} -ValidateOnly | Should -BeFalse + $Result = @{ a = 1 } | Test-ObjectGraph @{} $Result | Should -BeOfType PSCustomObject $Result.Valid | Should -BeFalse $Result.ObjectNode.Path | Should -BeNullOrEmpty - $Result.Issue | Should -BeLike "*not accepted*a*" + $Result.Issue | Should -BeLike "Node 'a' is denied" } It "[X] Complex object" { - $Person | Test-Object @{} -ValidateOnly | Should -BeFalse - $Result = $Person | Test-Object @{} + $Person | Test-ObjectGraph @{} -ValidateOnly | Should -BeFalse + $Result = $Person | Test-ObjectGraph @{} $Result | Should -BeOfType PSCustomObject + $Result | Should -HaveCount 9 $Result.Valid | Should -BeFalse - $Result.ObjectNode.Path | Should -BeNullOrEmpty - $Result.Issue | Should -BeLike "*not accepted*FirstName*LastName*" + $Result.Issue | Should -BeLike "Node * is denied" } It "[X] Complex map node" { - $Person | Test-Object @{ '@type' = [PSMapNode] } -ValidateOnly | Should -BeFalse - $Result = $Person | Test-Object @{ '@type' = [PSMapNode] } + $Person | Test-ObjectGraph @{ '@type' = [PSMapNode] } -ValidateOnly | Should -BeFalse + $Result = $Person | Test-ObjectGraph @{ '@type' = [PSMapNode] } $Result | Should -BeOfType PSCustomObject + $Result | Should -HaveCount 9 $Result.Valid | Should -BeFalse - $Result.ObjectNode.Path | Should -BeNullOrEmpty - $Result.Issue | Should -BeLike "*not accepted*FirstName*LastName*" + $Result.Issue | Should -BeLike "Node * is denied" } } Context 'Any (child) Node' { It "[V] Leaf node" { - 'Test' | Test-Object @() -ValidateOnly | Should -BeTrue - 'Test' | Test-Object @() | Should -BeNullOrEmpty + 'Test' | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + 'Test' | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Empty list node" { - ,@() | Test-Object @() -ValidateOnly | Should -BeTrue - ,@() | Test-Object @() | Should -BeNullOrEmpty + , @() | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + , @() | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Simple list node" { - ,@('Test') | Test-Object @() -ValidateOnly | Should -BeTrue - ,@('Test') | Test-Object @() | Should -BeNullOrEmpty + , @('Test') | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + , @('Test') | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Simple list node" { - ,@('a', 'b') | Test-Object @() -ValidateOnly | Should -BeTrue - ,@('a', 'b') | Test-Object @() | Should -BeNullOrEmpty + , @('a', 'b') | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + , @('a', 'b') | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Empty map node" { - @{} | Test-Object @() -ValidateOnly | Should -BeTrue - @{} | Test-Object @() | Should -BeNullOrEmpty + @{} | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + @{} | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Simple map node" { - @{ a = 1 } | Test-Object @() -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object @() | Should -BeNullOrEmpty + @{ a = 1 } | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Complex object" { - $Person | Test-Object @() -ValidateOnly | Should -BeTrue - $Person | Test-Object @() | Should -BeNullOrEmpty + $Person | Test-ObjectGraph @() -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @() | Should -BeNullOrEmpty } It "[V] Complex map node" { - $Person | Test-Object @{ '@type' = [PSMapNode]; '*' = @() } -ValidateOnly | Should -BeFalse - $Person | Test-Object @{ '@type' = [PSMapNode]; '@AllowExtraNodes' = $true } | Should -BeNullOrEmpty + $Person | Test-ObjectGraph @{ '@type' = [PSMapNode]; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ '@type' = [PSMapNode]; '@AnyName' = @() } | Should -BeNullOrEmpty } } Context 'Map nodes' { It "[V] Single name" { - $Person | Test-Object @{ Age = @{ '@Type' = 'Int' }; '@AllowExtraNodes' = $true } -ValidateOnly | Should -BeTrue - $Person | Test-Object @{ Age = @{ '@Type' = 'Int' }; '@AllowExtraNodes' = $true } | Should -BeNullOrEmpty + $Person | Test-ObjectGraph @{ Age = @{ '@Type' = 'Int' }; '@AnyName' = @() } -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph @{ Age = @{ '@Type' = 'Int' }; '@AnyName' = @() } | Should -BeNullOrEmpty } It "[V] Multiple names" { $Schema = @{ - FirstName = @{ '@Type' = 'String' } - LastName = @{ '@Type' = 'String' } - IsAlive = @{ '@Type' = 'Bool' } - Birthday = @{ '@Type' = 'DateTime' } - Age = @{ '@Type' = 'Int' } - '@AllowExtraNodes' = $true + FirstName = @{ '@Type' = 'String' } + LastName = @{ '@Type' = 'String' } + IsAlive = @{ '@Type' = 'Bool' } + Birthday = @{ '@Type' = 'DateTime' } + Age = @{ '@Type' = 'Int' } + '@AnyName' = @() } - $Person | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Person | Test-Object $Schema | Should -BeNullOrEmpty + $Person | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] All (root) names defined" { @@ -616,13 +730,13 @@ Describe 'Test-Object' { IsAlive = @{ '@Type' = 'Bool' } Birthday = @{ '@Type' = 'DateTime' } Age = @{ '@Type' = 'Int' } - Address = @{ '@Type' = 'PSMapNode', $Null; '@AllowExtraNodes' = $true } - Phone = @{ '@Type' = 'PSMapNode', $Null; '@AllowExtraNodes' = $true } - Children = @{ '@Type' = 'PSListNode', $Null; '@AllowExtraNodes' = $true } - Spouse = @{ '@Type' = 'String', $Null } + Address = @{ '@Type' = 'PSMapNode', $Null; '@AnyName' = @() } + Phone = @{ '@Type' = 'PSMapNode', $Null; '@AnyName' = @() } + Children = @{ '@Type' = 'PSListNode', $Null; '@AnyName' = @() } + Spouse = @{ '@Type' = 'String', $Null } } - $Person | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Person | Test-Object $Schema | Should -BeNullOrEmpty + $Person | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } } @@ -631,7 +745,7 @@ Describe 'Test-Object' { BeforeAll { $Data = @{ - fruits = @( + fruits = @( 'apple', 'orange', 'pear' @@ -649,7 +763,7 @@ Describe 'Test-Object' { } $Data2 = @{ - fruits = @( + fruits = @( 'apple', 'orange', 'pear' @@ -675,244 +789,243 @@ Describe 'Test-Object' { $Schema = @{ vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeFalse - $Data | Test-Object $Schema | Should -not -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + $Data | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } It "[V] One specific node and allowing extra nodes" { $Schema = @{ - '@AllowExtraNodes' = $true + '@AnyName' = @() vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] One specific node and allowing extra nodes" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ - '@Type' = [PSListNode] - '@AllowExtraNodes' = $true - veggie = @{ - '@Type' = [PSMapNode] + '@Type' = [PSListNode] + '@AnyName' = @() + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Single node that match a test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[X] Multiple nodes that match at least one single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] - '@AllowExtraNodes' = $true + veggie = @{ + '@Type' = [PSMapNode] + '@AnyName' = @() veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Multiple nodes that match a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Multiple nodes that match a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] veggie1 = @{ - '@Type' = [PSMapNode] - '@Unique' = $true + '@Type' = [PSMapNode] + '@Unique' = $true veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } veggie2 = @{ - '@Type' = [PSMapNode] - '@Unique' = $true + '@Type' = [PSMapNode] + '@Unique' = $true veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Multiple nodes that match a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] + veggie = @{ + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data2 | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data2 | Test-Object $Schema | Should -BeNullOrEmpty + $Data2 | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data2 | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Match at least one node in a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] - '@AllowExtraNodes' = $true + veggie = @{ + '@Type' = [PSMapNode] + '@AnyName' = @() veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data2 | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data2 | Test-Object $Schema | Should -BeNullOrEmpty + $Data2 | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data2 | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Duplicate nodes that match a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - '@AllowExtraNodes' = $true - veggie = @{ - '@Type' = [PSMapNode] - '@Unique' = $true + veggie = @{ + '@Type' = [PSMapNode] + '@Unique' = $true veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data2 | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data2 | Test-Object $Schema | Should -BeNullOrEmpty + $Data2 | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + $Data2 | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } It "[X] Duplicate nodes that match a single test definition" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] - veggie = @{ - '@Type' = [PSMapNode] - '@Unique' = $true + veggie = @{ + '@Type' = [PSMapNode] + '@Unique' = $true veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data2 | Test-Object $Schema -ValidateOnly | Should -BeFalse - $Data2 | Test-Object $Schema | Should -not -BeNullOrEmpty + $Data2 | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + $Data2 | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } It "[V] Multiple nodes that match equal test definitions" { $Schema = @{ - fruits = @{ + fruits = @{ '@Type' = [PSListNode] - fruit = @{ '@Type' = [String] } + fruit = @{ '@Type' = [String] } } vegetables = @{ '@Type' = [PSListNode] veggie1 = @{ - '@Type' = [PSMapNode] + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } veggie2 = @{ - '@Type' = [PSMapNode] + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } veggie3 = @{ - '@Type' = [PSMapNode] + '@Type' = [PSMapNode] veggieName = @{ '@Type' = [String] } veggieLike = @{ '@Type' = [Bool] } } } } - $Data2 | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data2 | Test-Object $Schema | Should -BeNullOrEmpty + $Data2 | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data2 | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } It "[V] Full assert test" { @@ -922,19 +1035,19 @@ Describe 'Test-Object' { IsAlive = @{ '@Type' = 'Bool' } Birthday = @{ '@Type' = 'DateTime' } Age = @{ - '@Type' = 'Int' + '@Type' = 'Int' '@Minimum' = 0 '@Maximum' = 99 } - Address = @{ - '@Type' = 'PSMapNode' + Address = @{ + '@Type' = 'PSMapNode' Street = @{ '@Type' = 'String' } City = @{ '@Type' = 'String' } State = @{ '@Type' = 'String' } PostalCode = @{ '@Type' = 'String' } } - Phone = @{ - '@Type' = 'PSMapNode', $Null + Phone = @{ + '@Type' = 'PSMapNode', $Null Home = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } Mobile = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } Work = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } @@ -942,116 +1055,178 @@ Describe 'Test-Object' { Children = @(@{ '@Type' = 'String', $Null }) Spouse = @{ '@Type' = 'String', $Null } } - $Person | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Person | Test-Object $Schema | Should -BeNullOrEmpty + $Person | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph $Schema | Should -BeNullOrEmpty } } - Context 'Required node' { + Context 'Optional node' { - $Schema = @{ - Id = @{ '@Type' = 'Int'; '@Required' = $true } - Data = @{ '@Type' = 'String' } + It 'Required Id, Optional Data' { + $Schema = @{ + Id = @{ '@Type' = 'Int' } + Data = @{ '@Type' = 'String'; '@Optional' = $true } + } + + @{ Id = 42 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Id = 42; Data = 'Test' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Data = 'Test' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ Id = 42; Test = 'Test' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse } + } + + Context 'Single optional node' { - @{ Id = 42 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ Id = 42; Data = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ Data = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ Id = 42; Test = 'Test' } | Test-Object $Schema -ValidateOnly | Should -BeFalse + It 'Required Id, Optional Data' { + $Schema = @{ + One = @{ '@Type' = 'String'; '@Optional' = $true } + } + + @{ One = 'Test' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Two = 'Test' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{} | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + } } - Context 'Required nodes formula' { + Context 'Nodes condition' { - it 'Not' { + It 'Not' { $Schema = @{ - a = @{ '@Type' = 'Int' } - '@RequiredNodes' = 'not a' + a = @{ '@Type' = 'Int' } + '@Requires' = 'not a' } - @{ a = 1 } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object $Schema | Should -not -BeNullOrEmpty - @{ a = '1' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = '1' } | Test-Object $Schema | Should -not -BeNullOrEmpty + @{ a = 1 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + @{ a = '1' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = '1' } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } - it 'And' { + It 'And' { $Schema = @{ - a = @{ '@Type' = 'Int' } - b = @{ '@Type' = 'Int' } - '@RequiredNodes' = 'a and b' + a = @{ '@Type' = 'Int' } + b = @{ '@Type' = 'Int' } + '@Requires' = 'a and b' } - @{ a = 1 } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1 } | Test-Object $Schema | Should -not -BeNullOrEmpty - @{ b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ b = 2 } | Test-Object $Schema | Should -not -BeNullOrEmpty - @{ a = 1; b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ a = 1; b = 2 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ a = 1; b = '2' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = '2' } | Test-Object $Schema | Should -not -BeNullOrEmpty + @{ a = 1 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1 } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + @{ b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ b = 2 } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } - it 'Or' { + It 'Or' { $Schema = @{ - a = @{ '@Type' = 'Int' } - b = @{ '@Type' = 'Int' } - '@RequiredNodes' = 'a or b' + a = @{ '@Type' = 'Int' } + b = @{ '@Type' = 'Int' } + '@Requires' = 'a or b' } - @{ a = 1 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ b = 2 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ a = 1; b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ a = 1; b = 2 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ a = 1; b = '2' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = '2' } | Test-Object $Schema | Should -not -BeNullOrEmpty + @{ a = 1 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ b = 2 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } - it 'Xor' { + It 'Xor' { $Schema = @{ - a = @{ '@Type' = 'Int' } - b = @{ '@Type' = 'Int' } - '@RequiredNodes' = 'a xor b' + a = @{ '@Type' = 'Int' } + b = @{ '@Type' = 'Int' } + '@Requires' = 'a xor b' } - @{ a = 1 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ b = 2 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ a = 1; b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = 2 } | Test-Object $Schema | Should -not -BeNullOrEmpty - @{ a = 1; b = '2' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = '2' } | Test-Object $Schema | Should -not -BeNullOrEmpty + @{ a = 1 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ b = 2 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } - it 'Rambling Xor' { + It 'Rambling Xor' { $Schema = @{ - a = @{ '@Type' = 'Int' } - b = @{ '@Type' = 'Int' } - '@RequiredNodes' = '(a and not b) or (not a and b)' - } - @{ a = 1 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ a = 1 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeTrue - @{ b = 2 } | Test-Object $Schema | Should -BeNullOrEmpty - @{ a = 1; b = 2 } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = 2 } | Test-Object $Schema | Should -not -BeNullOrEmpty - @{ a = 1; b = '2' } | Test-Object $Schema -ValidateOnly | Should -BeFalse - @{ a = 1; b = '2' } | Test-Object $Schema | Should -not -BeNullOrEmpty + a = @{ '@Type' = 'Int' } + b = @{ '@Type' = 'Int' } + '@Requires' = '(a and not b) or (not a and b)' + } + @{ a = 1 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ a = 1 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ b = 2 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = 2 } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + @{ a = 1; b = '2' } | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty + } + + It 'Negate node branch' { + $Schema = @{ + Parent = @{ + anInt = @{ '@Type' = 'Int' } + } + } + @{ Parent = 1, 2, 3 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Parent = 1, 2, 3 } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + + $Schema = @{ + Parent = @{ # One of the nodes should not be a string + '@Requires' = 'Not aString' + '@AnyName' = @() + aString = @{ '@Type' = 'String' } + } + } + @{ Parent = 1 , 2 , 3 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Parent = '1', 2 , 3 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Parent = '1', '2', 3 } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Parent = '1', '2', '3' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + } + } + + Context 'Ordered' { + + $Schema = @{ + Parent = [PSCustomObject]@{ + '@Ordered' = $True + 1 = @{ '@Like' = 'One' } + 2 = @{ '@Like' = 'Two' } + 3 = @{ '@Like' = 'Three' } + } + } + + It 'In order' { + @{ Parent = 'One', 'Two', 'Three' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{ Parent = 'One', 'Two', 'Three' } | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + } + + It 'In order' { + @{ Parent = 'Three', 'Two', 'One' } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + $Issues = @{ Parent = 'Three', 'Two', 'One' } | Test-ObjectGraph $Schema | Select-Issue + $Issues.Count | Should -be 2 + $Issues | Should -Contain "The value 'Three' is not like 'One'" + $Issues | Should -Contain "The value 'One' is not like 'Three'" } } Context 'Unique child nodes' { - it 'Unique' { + It 'Unique' { $Schema = @{ - '@Type' = [PSListNode] + '@Type' = [PSListNode] Children = @{'@Type' = [String]; '@Unique' = $true } } - ,@('a', 'b', 'c') | Test-Object $Schema -ValidateOnly | Should -BeTrue - ,@('a', 'b', 'c') | Test-Object $Schema | Should -BeNullOrEmpty - ,@('a', 'b', 'a') | Test-Object $Schema -ValidateOnly | Should -BeFalse - ,@('a', 'b', 'a') | Test-Object $Schema | Should -not -BeNullOrEmpty + , @('a', 'b', 'c') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + , @('a', 'b', 'c') | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + , @('a', 'b', 'a') | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + , @('a', 'b', 'a') | Test-ObjectGraph $Schema | Should -Not -BeNullOrEmpty } - it 'Unique collection' { + It 'Unique collection' { $Schema = @{ EnabledServers = @(@{'@Type' = 'String'; '@Unique' = 'Server' }) DisabledServers = @(@{'@Type' = 'String'; '@Unique' = 'Server' }) @@ -1060,17 +1235,17 @@ Describe 'Test-Object' { EnabledServers = 'NL1234', 'NL1235', 'NL1236' DisabledServers = 'NL1237', 'NL1238', 'NL1239' } - $Servers | Test-Object $Schema -ValidateOnly | Should -BeTrue + $Servers | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue $Servers = @{ EnabledServers = 'NL1234', 'NL1235', 'NL1236' DisabledServers = 'NL1237', 'NL1235', 'NL1239' } - $Servers | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Servers | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse $Results = $Servers | Test-ObjectGraph $Schema $Results[0].Issue | Should -BeLike '*equal to the node*' } - it 'Unique decedents' { + It 'Unique decedents' { $Schema = @{ BookStore = @( @{ @@ -1097,7 +1272,7 @@ Describe 'Test-Object' { } ) } - $Books | Test-Object $Schema -ValidateOnly | Should -BeTrue + $Books | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue $Books = @{ BookStore = @( @{ @@ -1120,7 +1295,7 @@ Describe 'Test-Object' { } ) } - $Books | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Books | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse } } @@ -1129,97 +1304,109 @@ Describe 'Test-Object' { BeforeAll { $Schema = @{ - '@Type' = [PSMapNode] + '@Type' = [PSMapNode] '@References' = @{ - Name = @{ '@Type' = 'String'; '@Match' = '\w{3,16}' } + Name = @{ '@Type' = 'String'; '@Match' = '\w{3,16}' } Address = @{ - '@Type' = [PSMapNode] + '@Type' = [PSMapNode] Street = @{ '@Type' = 'String' } City = @{ '@Type' = 'String' } State = @{ '@Type' = 'String' } PostalCode = @{ '@Type' = 'String' } } } - Buyer = @{ - '@Type' = [PSMapNode] - FirstName = 'Name' - LastName = 'Name' + Buyer = @{ + '@Type' = [PSMapNode] + FirstName = 'Name' + LastName = 'Name' ShippingAddress = 'Address' BillingAddress = 'Address' } } + $PSSchema = $Schema | Copy-Object -MapAs PSCustomObject + $RecurseSchema = @{ '@References' = @{ Item = @{ - '@AllowExtraNodes' = $true - Id = @{ '@Match' = '^ID\d{6}$'; '@Required' = $true } - Data = 'Item' + '@Optional' = $true + '@AnyName' = @() + Id = @{ '@Match' = '^ID\d{6}$' } + Data = 'Item' } } - Test = 'Item' + Test = 'Item' } } - it '[V] Buyer' { + It '[V] Buyer' { $Data = - @{ - Buyer = @{ - FirstName = 'John' - LastName = 'Doe' - ShippingAddress = @{ - Street = '123 Main St' - City = 'AnyTown' - State = 'CA' - PostalCode = '12345' - } - BillingAddress = @{ - Street = '456 Elm St' - City = 'OtherTown' - State = 'CA' - PostalCode = '67890' - } + @{ + Buyer = @{ + FirstName = 'John' + LastName = 'Doe' + ShippingAddress = @{ + Street = '123 Main St' + City = 'AnyTown' + State = 'CA' + PostalCode = '12345' + } + BillingAddress = @{ + Street = '456 Elm St' + City = 'OtherTown' + State = 'CA' + PostalCode = '67890' } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Data | Test-Object $Schema | Should -BeNullOrEmpty + } + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + + $Data | Test-ObjectGraph $PSSchema -ValidateOnly | Should -BeTrue + $Data | Test-ObjectGraph $PSSchema | Should -BeNullOrEmpty } - it '[X] Buyer - incorrect LastName' { + It '[X] Buyer - incorrect LastName' { $Data = - @{ - Buyer = @{ - FirstName = 'John' - LastName = 'Do' # Required 3-16 chars - ShippingAddress = @{ - Street = '123 Main St' - City = 'AnyTown' - State = 'CA' - PostalCode = '12345' - } - BillingAddress = @{ - Street = '456 Elm St' - City = 'OtherTown' - State = 'CA' - PostalCode = '67890' - } + @{ + Buyer = @{ + FirstName = 'John' + LastName = 'Do' # Required 3-16 chars + ShippingAddress = @{ + Street = '123 Main St' + City = 'AnyTown' + State = 'CA' + PostalCode = '12345' + } + BillingAddress = @{ + Street = '456 Elm St' + City = 'OtherTown' + State = 'CA' + PostalCode = '67890' } } - $Data | Test-Object $Schema -ValidateOnly | Should -BeFalse - $Result = $Data | Test-Object $Schema - $Result | Should -not -BeNullOrEmpty + } + $Data | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse + $Result = $Data | Test-ObjectGraph $Schema + $Result | Should -Not -BeNullOrEmpty + $Result.ObjectNode.Path | Should -Contain 'Buyer.LastName' + $Result.ObjectNode.Value | Should -Contain 'Do' + + $Data | Test-ObjectGraph $PSSchema -ValidateOnly | Should -BeFalse + $Result = $Data | Test-ObjectGraph $PSSchema + $Result | Should -Not -BeNullOrEmpty $Result.ObjectNode.Path | Should -Contain 'Buyer.LastName' $Result.ObjectNode.Value | Should -Contain 'Do' } - it '[V] Recursive reference' { + It '[V] Recursive reference' { $Data = @{ Test = @{ - Id = 'ID000001' + Id = 'ID000001' Data = @{ - Id = 'ID000002' + Id = 'ID000002' Data = @{ Id = 'ID000003' } @@ -1227,74 +1414,583 @@ Describe 'Test-Object' { } } - $Data | Test-Object $RecurseSchema -ValidateOnly | Should -be $true + $Data | Test-ObjectGraph $RecurseSchema -ValidateOnly | Should -Be $true } - it '[V] Recursive object' -Skip:$($PSVersionTable.PSVersion -lt '6.0') { + It '[X] Recursive PSItem object (required Drives)' -Skip:$($PSVersionTable.PSVersion -lt '6.0') { $Schema = @{ '@References' = @{ RecursePSDrive = @{ - '@AllowExtraNodes' = $true - Name = @{ '@Type' = [String] } - Root = @{ '@Type' = [String] } - Used = @{ '@Type' = [Long] } - Provider = @{ - '@AllowExtraNodes' = $true - Drives = @{ + '@AnyName' = @() + Name = @{ '@Type' = [String] } + Root = @{ '@Type' = [String] } + Used = @{ '@Type' = [Long], $Null } + Provider = @{ + '@AnyName' = @() + Drives = @{ '@Type' = [PSListNode] - '@AllowExtraNodes' = $true - Drive = 'RecursePSDrive' + Drive = 'RecursePSDrive' + } + } + } + } + '@AnyName' = @() + Mode = @{ '@Like' = '?????' } + LastWriteTime = @{ '@Type' = [DateTime] } + Exists = @{ '@Type' = [Bool] } + Name = @{ '@Type' = [String] } + PSDrive = 'RecursePSDrive' + } + + $Warning = & { Get-Item / | Test-ObjectGraph $Schema -Depth 5 -ValidateOnly | Should -BeFalse } 3>&1 + $Warning | Should -BeLike '*reached the maximum depth of 5*' + $Ref = @{ Result = $null } + $Warning = & { $Ref.Result = Get-Item / | Test-ObjectGraph $Schema -Depth 5 } 3>&1 + $Warning | Should -BeLike '*reached the maximum depth of 5*' + $Ref.Result.Count | Should -BeGreaterOrEqual 1 + $Ref.Result[0].ObjectNode.Path | Should -Be 'PSDrive.Provider.Drives[0].Provider' + $Ref.Result[0].Valid | Should -BeFalse + $Ref.Result[0].Issue | Should -BeLike "The requirement *'Drives'* is not met" + } + + It '[V] Recursive PSItem object (optional Drives)' -Skip:$($PSVersionTable.PSVersion -lt '6.0') { + $Schema = @{ + '@References' = @{ + RecursePSDrive = @{ + '@AnyName' = @() + Name = @{ '@Type' = [String] } + Root = @{ '@Type' = [String] } + Used = @{ '@Type' = [Long], $Null } + Provider = @{ + '@AnyName' = @() + Drives = @{ + '@Type' = [PSListNode] + '@Optional' = $true + Drive = 'RecursePSDrive' + } + } + } + } + '@AnyName' = @() + Mode = @{ '@Like' = '?????' } + LastWriteTime = @{ '@Type' = [DateTime] } + Exists = @{ '@Type' = [Bool] } + Name = @{ '@Type' = [String] } + PSDrive = 'RecursePSDrive' + } + + $Warning = & { Get-Item / | Test-ObjectGraph $Schema -Depth 5 -ValidateOnly | Should -BeTrue } 3>&1 + $Warning | Should -BeLike '*reached the maximum depth of 5*' + $Ref = @{ Result = $null } + $Warning = & { $Ref.Result = Get-Item / | Test-ObjectGraph $Schema -Depth 5 } 3>&1 + $Warning | Should -BeLike '*reached the maximum depth of 5*' + $Ref.Result | Should -BeNullOrEmpty + } + + It '[V] Recursive PSItem object -Elaborate' -Skip:$($PSVersionTable.PSVersion -lt '6.0') { + $Schema = @{ + '@References' = @{ + RecursePSDrive = @{ + '@AnyName' = @() + Name = @{ '@Type' = [String] } + Root = @{ '@Type' = [String] } + Used = @{ '@Type' = [Long], $Null } + Provider = @{ + '@AnyName' = @() + Drives = @{ + '@Type' = [PSListNode] + '@Optional' = $true + Drive = 'RecursePSDrive' } } } } - '@AllowExtraNodes' = $true - Mode = @{ '@Like' = '?????' } - LastWriteTime = @{ '@Type' = [DateTime] } - Exists = @{ '@Type' = [Bool] } - Name = @{ '@Type' = [String] } - PSDrive = 'RecursePSDrive' + '@AnyName' = @() + Mode = @{ '@Like' = '?????' } + LastWriteTime = @{ '@Type' = [DateTime] } + Exists = @{ '@Type' = [Bool] } + Name = @{ '@Type' = [String] } + PSDrive = 'RecursePSDrive' + } + + $Ref = @{ Result = $null } + $Warning = & { $Ref.Result = Get-Item / | Test-ObjectGraph $Schema -Depth 5 -Elaborate } 3>&1 + # $Warning | Should -BeLike '*reached the maximum depth of 5*' + $Ref.Result.Count | Should -BeGreaterThan 10 + $Ref.Result.Valid | Should -BeTrue + $Ref.Result.ObjectNode.Path | Should -Contain 'PSDrive.Provider.Drives' + } + } + + Context 'Embedded keys' { + + It "Names" { + + $Schema = @{ + Names = @{ + First = @{ Id = @{ '@Like' = 'First' }; Value = @{ '@Type' = 'String' } } + Middle = @{ Id = @{ '@Like' = 'Middle' }; Value = @{ '@Type' = 'String' }; '@Optional' = $true } + Last = @{ Id = @{ '@Like' = 'Last' }; Value = @{ '@Type' = 'String' } } + } } - Get-Item / | Test-Object $Schema -Depth 5 -ValidateOnly -WarningAction SilentlyContinue | Should -BeTrue - $Result = Get-Item / | Test-Object $Schema -Depth 5 -Elaborate -WarningAction SilentlyContinue - $Result.ObjectNode.Path | Should -Contain 'PSDrive.Provider.Drives[0].Name' + @{Names = @(@{Id = 'First'; Value = 'John' }, @{Id = 'Last'; Value = 'Doe' }) } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{Names = @(@{Id = 'First'; Value = 'John' }, @{Id = 'Middle'; Value = 'M' }, @{Id = 'Last'; Value = 'Doe' }) } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + @{Names = @(@{Id = 'First'; Value = 'John' }, @{Id = 'Middle'; Value = 'M' }) } | Test-ObjectGraph $Schema -ValidateOnly | Should -BeFalse } } Context 'Different Assert Test Prefix' { - It "[V] AssertTestPrefix = '^'" { + It "[V] AssertPrefix = '^'" { $Schema = @{ - AssertTestPrefix = '^' - FirstName = @{ '^Type' = 'String' } - LastName = @{ '^Type' = 'String' } - IsAlive = @{ '^Type' = 'Bool' } - Birthday = @{ '^Type' = 'DateTime' } - Age = @{ - '^Type' = 'Int' + AssertPrefix = '^' + FirstName = @{ '^Type' = 'String' } + LastName = @{ '^Type' = 'String' } + IsAlive = @{ '^Type' = 'Bool' } + Birthday = @{ '^Type' = 'DateTime' } + Age = @{ + '^Type' = 'Int' '^Minimum' = 0 '^Maximum' = 99 } - Address = @{ - '^Type' = 'PSMapNode' + Address = @{ + '^Type' = 'PSMapNode' Street = @{ '^Type' = 'String' } City = @{ '^Type' = 'String' } State = @{ '^Type' = 'String' } PostalCode = @{ '^Type' = 'String' } } - Phone = @{ - '^Type' = 'PSMapNode', $Null - Home = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AllowExtraNodes' = $true } - Mobile = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AllowExtraNodes' = $true } - Work = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AllowExtraNodes' = $true } + Phone = @{ + '^Type' = 'PSMapNode', $Null + Home = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AnyName' = @() } + Mobile = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AnyName' = @() } + Work = @{ '^Type' = 'String', 'PSListNode'; '^Match' = '^\d{3} \d{3}-\d{4}$'; '^AnyName' = @() } + } + Children = @(@{ '^Type' = 'String', $Null }) + Spouse = @{ '^Type' = 'String', $Null } + } + $Person | Test-ObjectGraph $Schema -ValidateOnly | Should -BeTrue + $Person | Test-ObjectGraph $Schema | Should -BeNullOrEmpty + } + + } + + Context 'Error handling' { + + It 'Unknown assert' { + { 1 | Test-ObjectGraph @{ '@Type' = [int]; '@UnknownAssert' = $true } } | Should -Throw -ExpectedMessage "*Unknown Assert: 'UnknownAssert'" + } + } + + #Region Github issues + + Context '#126 [Test-ObjectGraph] Parents of failing item are also included in the output' { + + It 'IncorrectParameterName' { + $data = @{ + NonNodeData = @{ + AzureAD = @{ + IncorrectParameterName = @( + @{ + Param1 = 8 + } + ) + } + } + } + + $Schema = @{ + NonNodeData = @{ + '@Type' = 'PSMapNode' + AzureAD = @{ + '@Type' = 'PSMapNode' + } + } + } + + $data | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Result = $data | Test-Object $Schema + $Result | Should -HaveCount 1 + } + } + + Context '#129 [Test-ObjectGraph] When the schema specifies that a parameter is required and another parameter is not correct, that other parameter is not listed as failed.' { + + It 'DefaultLength = [string]' { + $Schema = @{ + NonNodeData = @{ + '@Type' = 'PSMapNode' + AzureAD = @{ + '@Type' = 'PSMapNode' + AuthenticationMethodPolicy = @( + @{ + '@Type' = 'PSMapNode' + DefaultLength = @{ '@Type' = 'Int' } + DefaultLifetimeInMinutes = @{ '@Type' = 'Int' } + Ensure = @{ '@Type' = 'String'; '@Optional' = $true } + Id = @{ '@Type' = 'String'; '@Optional' = $true } + IncludeTargets = @{ + '@Optional' = $true + target = @{ + '@Type' = 'PSMapNode' + Id = @{ '@Type' = 'String' } + TargetType = @{ '@Type' = 'String' } + } + } + IsUsableOnce = @{ '@Type' = 'Bool'; '@Optional' = $true } + MaximumLifetimeInMinutes = @{ '@Type' = 'Int'; '@Optional' = $true } + MinimumLifetimeInMinutes = @{ '@Type' = 'Int'; '@Optional' = $true } + State = @{ '@Type' = 'String'; '@Optional' = $true } + } + ) + } + } + } + + $data = @{ + NonNodeData = @{ + AzureAD = @{ + AuthenticationMethodPolicy = @( + @{ + DefaultLength = 'string' + DoesNotExist = 'string' + DefaultLifetimeInMinutes = 10 + } + ) + } } - Children = @(@{ '^Type' = 'String', $Null }) - Spouse = @{ '^Type' = 'String', $Null } } - $Person | Test-Object $Schema -ValidateOnly | Should -BeTrue - $Person | Test-Object $Schema | Should -BeNullOrEmpty + + $data | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = $data | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 2 + $Issues | Should -Contain "Node 'DoesNotExist' is denied" + $Issues | Should -Contain "'string' is not of type 'Int'" } + } + + Context '#129 Using @Requires' { + + It 'DefaultLength = [string]' { + $Schema = @{ + NonNodeData = @{ + '@Type' = 'PSMapNode' + AzureAD = @{ + '@Type' = 'PSMapNode' + AuthenticationMethodPolicy = @( + @{ + '@Type' = 'PSMapNode' + '@Requires' = 'DefaultLength', 'DefaultLifetimeInMinutes' + DefaultLength = @{ '@Type' = 'Int' } + DefaultLifetimeInMinutes = @{ '@Type' = 'Int' } + Ensure = @{ '@Type' = 'String' } + Id = @{ '@Type' = 'String' } + IncludeTargets = @{ + target = @{ + '@Type' = 'PSMapNode' + Id = @{ '@Type' = 'String' } + TargetType = @{ '@Type' = 'String' } + } + } + IsUsableOnce = @{ '@Type' = 'Bool' } + MaximumLifetimeInMinutes = @{ '@Type' = 'Int' } + MinimumLifetimeInMinutes = @{ '@Type' = 'Int' } + State = @{ '@Type' = 'String' } + } + ) + } + } + } + + $data = @{ + NonNodeData = @{ + AzureAD = @{ + AuthenticationMethodPolicy = @( + @{ + DefaultLength = 'string' + DoesNotExist = 'string' + DefaultLifetimeInMinutes = 10 + } + ) + } + } + } + + $data | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = $data | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 2 + $Issues | Should -Contain "Node 'DoesNotExist' is denied" + $Issues | Should -Contain "'string' is not of type 'Int'" + } + + It '#129 [V] A FMO application' { + + $Schema = @{ + Company = @{ + '@Type' = 'Array' + '@AnyName' = @() + FMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'FMO' } + Id = @{ '@Type' = 'Int'; '@Minimum' = 2000 } + } + } + } + + $Data = @{ + Company = @( + @{ Env = 'CMO'; Id = 1234 }, + @{ Env = 'FMO'; Id = 1235 }, + @{ Env = 'FMO'; Id = 2345 } + ) + } + + $data | Test-Object $Schema -ValidateOnly | Should -BeTrue + $data | Test-Object $Schema | Should -BeNullOrEmpty + } + + It '#129 [X] A FMO application' { + $Schema = @{ + Company = @{ + '@Type' = 'Array' + '@AnyName' = @() + FMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'FMO' } + Id = @{ '@Type' = 'Int'; '@Minimum' = 2000 } + } + } + } + + $Data = @{ + Company = @( + @{ Env = 'CMO'; Id = 1234 }, + @{ Env = 'FMO'; Id = 1235 }, + @{ Env = 'XMO'; Id = 2345 } # Env name typo + ) + } + + $Data | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Data | Test-Object $Schema | Select-Issue | Should -Be "The requirement 'FMO' is missing" + } + + It '#129 [V] A Company wide application' { + + $Schema = @{ + Company = @{ + '@Type' = 'Array' + '@Requires' = 'CMO or FMO' + CMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'CMO' } + Id = @{ '@Type' = 'Int'; '@Maximum' = 1999 } + } + FMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'FMO' } + Id = @{ '@Type' = 'Int'; '@Minimum' = 2000 } + } + } + } + + $Data = @{ + Company = @( + @{ Env = 'CMO'; Id = 1234 }, + @{ Env = 'FMO'; Id = 2345 }, + @{ Env = 'FMO'; Id = 3456 } + ) + } + + $data | Test-Object $Schema -ValidateOnly | Should -BeTrue + $Result = $data | Test-Object $Schema | Should -BeNullOrEmpty + } + + It '#129 [X] A Company wide application' { + + $Schema = @{ + Company = @{ + '@Type' = 'Array' + '@Requires' = 'CMO or FMO' + CMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'CMO' } + Id = @{ '@Type' = 'Int'; '@Maximum' = 1999 } + } + FMO = @{ + Env = @{ '@Type' = 'String'; '@Like' = 'FMO' } + Id = @{ '@Type' = 'Int'; '@Minimum' = 2000 } + } + } + } + + $Data = @{ + Company = @( + @{ Env = 'CMO'; Id = 1234 }, + @{ Env = 'FMO'; Id = 1235 }, # Id should be >= 2000 + @{ Env = 'FMO'; Id = 2345 } + ) + } + + $data | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Result = $data | Test-Object $Schema + $Result.Count | Should -BeGreaterOrEqual 1 + # $Result | Select-Issue | Should -Contain "The value 1235 is less or equal than 2000" + } + + } + + Context '#142 @MinimumCount' { + + BeforeAll { + $Schema = @{ + 'Minimal 3 Strings' = @{ '@Type' = [String]; '@MinimumCount' = 3 } + 'Minimal 3 Integers' = @{ '@Type' = [Int]; '@MinimumCount' = 3 } + } + } + + It "1, 'two', 3, 'four'" { + , @(1, 'two', 3, 'four') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four') | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 2 + $Issues | Should -Contain "'Minimal 3 Strings' occurred less than 3 times" + $Issues | Should -Contain "'Minimal 3 Integers' occurred less than 3 times" + } + + It "1, 'two', 3, 'four', 5" { + , @(1, 'two', 3, 'four', 5) | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four', 5) | Test-Object $Schema | Select-Issue + $Issues | Should -Be "'Minimal 3 Strings' occurred less than 3 times" + } + + It "1, 'two', 3, 'four', 'five'" { + , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema | Select-Issue + $Issues | Should -Be "'Minimal 3 Integers' occurred less than 3 times" + } + + It 'Valid' { + , @(1, 'two', 3, 'four', 5, 'six') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 'two', 3, 'four', 5, 'six', 7) | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 'two', 3, 'four', 5, 'six', 7, 'Eight') | Test-Object $Schema -ValidateOnly | Should -BeTrue + } + } + + Context '#142 @MaximumCount' { + + BeforeAll { + $Schema = @{ + 'Maximal 2 Strings' = @{ '@Type' = [String]; '@MaximumCount' = 2 } + 'Maximal 2 Integers' = @{ '@Type' = [Int]; '@MaximumCount' = 2 } + } + } + + It 'Valid' { + , @(1, 'two') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 'two', 3) | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @('One', 2, 'Three') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 'two', 3, 'four') | Test-Object $Schema -ValidateOnly | Should -BeTrue + } + + It "1, 2" { + , @(1, 2) | Test-Object $Schema -ValidateOnly | Should -BeFalse + , @(1, 2) | Test-Object $Schema | Select-Issue | Should -Be "The requirement 'Maximal 2 Strings' is missing" + } + + It "'one', 'two'" { + , @('one', 'two') | Test-Object $Schema -ValidateOnly | Should -BeFalse + , @('one', 'two') | Test-Object $Schema | Select-Issue | Should -Be "The requirement 'Maximal 2 Integers' is missing" + } + + It "1, 'two', 3, 'four', 5" { + , @(1, 'two', 3, 'four', 5) | Test-Object $Schema -ValidateOnly | Should -BeFalse + , @(1, 'two', 3, 'four', 5) | Test-Object $Schema | Select-Issue | Should -Be "'Maximal 2 Integers' occurred more than 2 times" + } + + It "1, 'two', 3, 'four', 'five'" { + , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema -ValidateOnly | Should -BeFalse + , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema | Select-Issue | Should -Be "'Maximal 2 Strings' occurred more than 2 times" + } + + It "1, 'two', 3, 'four', 5, 'Six'" { + , @(1, 'two', 3, 'four', 5, 'Six') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four', 5, 'Six') | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 2 + $Issues[0] | Should -BeLike "'*' occurred more than 2 times" + $Issues[1] | Should -BeLike "* is not of type '*'" + } + } + + Context '#142 @Count' { + + BeforeAll { + $Schema = @{ + '3 Strings' = @{ '@Type' = [String]; '@Count' = 3 } + '3 Integers' = @{ '@Type' = [Int]; '@Count' = 3 } + } + } + + It "1, 'two', 3, 'four'" { + , @(1, 'two', 3, 'four') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four') | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 4 + $Issues | Should -Contain "'3 Strings' occurred more than 3 times" + $Issues | Should -Contain "1 is not of type 'string'" + $Issues | Should -Contain "3 is not of type 'string'" + $Issues | Should -Contain "'3 Integers' occurred less than 3 times" + } + + It "1, 'two', 3, 'four', 5" { + , @(1, 'two', 3, 'four', 5) | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four', 5) | Test-Object $Schema | Select-Issue + $Issues | Should -Be "'3 Strings' occurred less than 3 times" + } + + It "1, 'two', 3, 'four', 'five'" { + , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 3, 'four', 'five') | Test-Object $Schema | Select-Issue + $Issues | Should -Be "'3 Integers' occurred less than 3 times" + } + + It 'Valid' { + , @(1, 'two', 3, 'four', 5, 'Six') | Test-Object $Schema -ValidateOnly | Should -BeTrue + } + + It "1, 2, 3, 4, 'Five'" { + , @(1, 2, 3, 4, 'Five') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 2, 3, 4, 'Five') | Test-Object $Schema | Select-Issue + $Issues.Count | Should -Be 2 + $Issues | Should -Contain "'3 Strings' occurred less than 3 times" + $Issues | Should -Contain "3 is not of type 'string'" + } + + It "1, 'two', 'three', 'four', 'five'" { + , @(1, 'two', 'three', 'four', 'five') | Test-Object $Schema -ValidateOnly | Should -BeFalse + $Issues = , @(1, 'two', 'three', 'four', 'five') | Test-Object $Schema | Select-Issue + $Issues | Should -Be "'3 Integers' occurred less than 3 times" + } + } + + Context '#142 @Conditional @MaximumCount' { + + BeforeAll { + $Schema = @{ + 'Maximal 2 Strings' = @{ '@Type' = [String]; '@Optional' = $true; '@MaximumCount' = 2 } + 'Maximal 2 Integers' = @{ '@Type' = [Int]; '@Optional' = $true; '@MaximumCount' = 2 } + } + } + + It 'Valid' { + , @('') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1) | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @('One') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1,2) | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 'Two') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 2, 'Three') | Test-Object $Schema -ValidateOnly | Should -BeTrue + , @(1, 2, 'Three', 'Four') | Test-Object $Schema -ValidateOnly | Should -BeTrue + } + + It 'Too many Integers' { + ,@(1, 2 ,3) | Test-Object $Schema | Select-Issue | Should -be "'Maximal 2 Integers' occurred more than 2 times" + ,@(1, 2 ,3, 'four') | Test-Object $Schema | Select-Issue | Should -be "'Maximal 2 Integers' occurred more than 2 times" + ,@(1, 2 ,3, 'Four', 'Five') | Test-Object $Schema | Select-Issue | Should -be "'Maximal 2 Integers' occurred more than 2 times" + } } + #EndRegion Github issues } \ No newline at end of file diff --git a/Tests/Test.Windows&Core.ps1 b/Tests/Test.Windows&Core.ps1 index 66f5695..df51a24 100644 --- a/Tests/Test.Windows&Core.ps1 +++ b/Tests/Test.Windows&Core.ps1 @@ -3,7 +3,7 @@ $Expression = { $Version = $PSVersionTable.PSVersion Import-Module $TestFolder\.. -Force Get-ChildItem -Path $TestFolder -Filter *.Tests.ps1 | - ForEach-Object { + ForEach-Object { $InformationRecord = . $_.FullName *>&1 foreach ($Message in $InformationRecord.MessageData.Message) { if ($Message -match '^\S*\[\+\]') { @@ -12,7 +12,7 @@ $Expression = { } elseif ($Message -match '^\S*\[-\]([^\r\n]*)') { Write-Host -NoNewline "$Version " - Write-Host -ForegroundColor Red "$($_.BaseName) $($Matches[1])" + Write-Host -BackgroundColor Red "$($_.BaseName) $($Matches[1])" } } } diff --git a/WhatsNew.md b/WhatsNew.md index 248cc5f..f87c1f4 100644 --- a/WhatsNew.md +++ b/WhatsNew.md @@ -1,3 +1,30 @@ +## 2025-04-05 0.3.2 + -Fixes + - #96 .Get_Value() return more than just the Value + - #97 RuntimeTypes don't properly ConvertTo-Expression + - #116 Use comma operator , for (embedded) empty arrays + - #119 IncludeUnderlying switch to Get-Node: Use two tilde `~~` to access all offspring nodes + - #124 [Test-ObjectGraph] Add possibility to allow removal of text formatting + - #125 [Test-ObjectGraph] Long parameter names are shortened in the Issue output + - #126 [Test-ObjectGraph] Parents of failing item are also included in the output + - #128 [Test-ObjectGraph] When two objects are failing and specifying Elaborate, the failing objects are listed twice + - #129 [Test-ObjectGraph] When the schema specifies that a parameter is required and another parameter is not correct, that other parameter is not listed as failed. + - #136 When schema child nodes are defined, any collection should be accepted. #136 + + - Enhancements + - #32 Add remove method + - #98 Pass on Add and Remove methods to PSCollectionNode + - #131 Test-ObjectGraph: use class for output + - #132 Display child value when it concerns a [PSLeafNode] + +## 2025-04-05 0.3.2-Preview3 (iRon) + - Fixes + - #121 Fixed merge -PrimaryKey bug + +## 2025-04-05 0.3.2-Preview (iRon) + - Fixes + - #120 Fixed auto-loading issues where types aren't known + ## 2025-04-05 0.3.1-Preview (iRon) - Fixes - #111 Compare-ObjectGraph fails with certain object graphs, presumably due to cyclical references diff --git a/_Temp/Compare-ObjectGraph.Tests.ps1 b/_Temp/Compare-ObjectGraph.Tests.ps1 deleted file mode 100644 index de48d35..0000000 --- a/_Temp/Compare-ObjectGraph.Tests.ps1 +++ /dev/null @@ -1,657 +0,0 @@ -#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"} - -using module ..\..\ObjectGraphTools - -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Reference', Justification = 'False positive')] -param() - -Describe 'Compare-ObjectGraph' { - - BeforeAll { - - Set-StrictMode -Version Latest - - $Reference = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - } - - Context 'Existence Check' { - - It 'Help' { - Compare-ObjectGraph -? | Out-String -Stream | Should -Contain SYNOPSIS - } - } - - Context 'Compare' { - - BeforeAll { - $Object12345 = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - @{ - Index = 4 - Name = 'Four' - Comment = 'Fourth item' - } - @{ - Index = 5 - Name = 'Five' - Comment = 'Fifth item' - } - ) - } - - function New-TestObject ($Indices) { - @{ - Comment = 'Sample ObjectGraph' - Data = @( - foreach ($Index in $Indices) { - $Object12345.Data[$Index - 1] - } - ) - } - } - } - - It 'Different comment string value' { - $Object = @{ - Comment = 'Something else' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Comment Value Sample ObjectGraph Something else' - } - - - It 'Object123' { - $Object123 = New-TestObject 1, 2, 3 - - $Object123 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $True - $Object123 | Compare-ObjectGraph $Reference | Should -BeNullOrEmpty - - $Object123 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $True - $Object123 | Compare-ObjectGraph $Reference -IgnoreListOrder | Should -BeNullOrEmpty - - $Object123 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $True - $Object123 | Compare-ObjectGraph $Reference -PrimaryKey Index | Should -BeNullOrEmpty - } - - It 'Object1' { - $Object1 = New-TestObject 1 - - $Object1 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object1 | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[1] Exists [hashtable]' - $Lines[4] | Should -be 'Data[2] Exists [hashtable]' - - $Object1 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object1 | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[1] Exists [hashtable]' - $Lines[4] | Should -be 'Data[2] Exists [hashtable]' - - $Object1 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object1 | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[1] Exists [hashtable]' - $Lines[4] | Should -be 'Data[2] Exists [hashtable]' - } - - It 'Object2' { - $Object2 = New-TestObject 2 - - $Object2 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object2 | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[0].Index Value 1 2' - $Lines[4] | Should -be 'Data[0].Comment Value First item Second item' - $Lines[5] | Should -be 'Data[0].Name Value One Two' - $Lines[6] | Should -be 'Data[1] Exists [hashtable]' - $Lines[7] | Should -be 'Data[2] Exists [hashtable]' - - $Object2 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object2 | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[0] Exists [hashtable]' - $Lines[4] | Should -be 'Data[2] Exists [hashtable]' - - $Object2 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object2 | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 1' - $Lines[3] | Should -be 'Data[0] Exists [hashtable]' - $Lines[4] | Should -be 'Data[2] Exists [hashtable]' - } - - It 'Object13' { - $Object13 = New-TestObject 1, 3 - - $Object13 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object13 | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1].Index Value 2 3' - $Lines[4] | Should -be 'Data[1].Comment Value Second item Third item' - $Lines[5] | Should -be 'Data[1].Name Value Two Three' - $Lines[6] | Should -be 'Data[2] Exists [hashtable]' - - $Object13 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object13 | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1] Exists [hashtable]' - - $Object13 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object13 | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1] Exists [hashtable]' - } - - It 'Object13a' { - $Object13a = New-TestObject 1, 3 - $Object13a.Data[1].Name = '3a' - - $Object13a | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object13a | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1].Index Value 2 3' - $Lines[4] | Should -be 'Data[1].Comment Value Second item Third item' - $Lines[5] | Should -be 'Data[1].Name Value Two 3a' - $Lines[6] | Should -be 'Data[2] Exists [hashtable]' - - $Object13a | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object13a | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1].Index Value 2 3' - $Lines[4] | Should -be 'Data[1].Comment Value Second item Third item' - $Lines[5] | Should -be 'Data[1].Name Value Two 3a' - $Lines[6] | Should -be 'Data[2] Exists [hashtable]' - - $Object13a | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object13a | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data Size 3 2' - $Lines[3] | Should -be 'Data[1].Name Value Three 3a' - $Lines[4] | Should -be 'Data[1] Exists [hashtable]' - } - - it 'Object321' { - $Object321 = New-TestObject 3, 2, 1 - - $Object321 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Index Value 1 3' - $Lines[3] | Should -be 'Data[0].Comment Value First item Third item' - $Lines[4] | Should -be 'Data[0].Name Value One 3a' - $Lines[5] | Should -be 'Data[2].Index Value 3 1' - $Lines[6] | Should -be 'Data[2].Comment Value Third item First item' - $Lines[7] | Should -be 'Data[2].Name Value Three One' - - $Object321 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Name Value Three 3a' - - $Object321 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Name Value Three 3a' - } - - it 'Object3a21' { - $Object321 = New-TestObject 3, 2, 1 - $Object13a.Data[0].Name = '3a' - - $Object321 | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Index Value 1 3' - $Lines[3] | Should -be 'Data[0].Comment Value First item Third item' - $Lines[4] | Should -be 'Data[0].Name Value One 3a' - $Lines[5] | Should -be 'Data[2].Index Value 3 1' - $Lines[6] | Should -be 'Data[2].Comment Value Third item First item' - $Lines[7] | Should -be 'Data[2].Name Value Three One' - - $Object321 | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference -IgnoreListOrder - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Name Value Three 3a' - - $Object321 | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $False - $Result = $Object321 | Compare-ObjectGraph $Reference -PrimaryKey Index - $Lines = ($Result | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Lines[0] | Should -be 'Path Discrepancy Reference InputObject' - $Lines[1] | Should -be '---- ----------- --------- -----------' - $Lines[2] | Should -be 'Data[0].Name Value Three 3a' - } - It 'Extra entry' { - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - @{ - Index = 4 - Name = 'Four' - Comment = 'Forth item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object | Compare-ObjectGraph $Reference - $Result.Count | Should -Be 2 - $Result[0].Path | Should -Be 'Data' - $Result[0].Discrepancy | Should -Be 'Size' - $Result[0].InputObject | Should -Be 4 - $Result[0].Reference | Should -Be 3 - $Result[1].Path | Should -Be 'Data[3]' - $Result[1].Discrepancy | Should -Be 'Exists' - $Result[1].InputObject | Should -Be '[HashTable]' - $Result[1].Reference | Should -Be $Null - } - It 'Different entry value' { - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Zero' # This is different - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Result = $Object | Compare-ObjectGraph $Reference - @($Result).Count | Should -Be 1 - $Result[0].Path | Should -Be 'Data[2].Name' - $Result[0].Discrepancy | Should -Be 'Value' - $Result[0].InputObject | Should -Be 'Zero' - $Result[0].Reference | Should -Be 'Three' - } - It 'Unordered array' { - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $False - $Object | Compare-ObjectGraph $Reference -IgnoreListOrder -IsEqual | Should -Be $True - $Object | Compare-ObjectGraph $Reference -PrimaryKey Index -IsEqual | Should -Be $True - $Result = $Object | Compare-ObjectGraph $Reference - $Result.Count | Should -Be 6 - } - It 'Unordered (hashtable) reference' { - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - [PSCustomObject]@{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - [PSCustomObject]@{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - [PSCustomObject]@{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $True - - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - [PSCustomObject]@{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - [PSCustomObject]@{ - Index = 2 - Comment = 'Second item' # Note: - Name = 'Two' # These entries are swapped - } - [PSCustomObject]@{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Reference -IsEqual | Should -Be $True - } - It 'Ordered (PSCustomObject) reference' { - $Ordered = @{ # Redefine Reference with order Dictionary/PSCustomObject - Comment = 'Sample ObjectGraph' - Data = @( - [PSCustomObject]@{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - [PSCustomObject]@{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - [PSCustomObject]@{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - [PSCustomObject]@{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - [PSCustomObject]@{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - [PSCustomObject]@{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Ordered -IsEqual | Should -Be $True - - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - [PSCustomObject]@{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - [PSCustomObject]@{ - Index = 2 - Comment = 'Second item' # Note: - Name = 'Two' # These entries are swapped - } - [PSCustomObject]@{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Ordered -IsEqual | Should -Be $True - $Object | Compare-ObjectGraph $Ordered -MatchMapOrder -IsEqual | Should -Be $False - $Object | Compare-ObjectGraph $Ordered -PrimaryKey Index -MatchMapOrder -IsEqual | Should -Be $False - $Result = $Object | Compare-ObjectGraph $Ordered -MatchMapOrder - $Result.Count | Should -Be 2 - } - } - - Context 'Issues' { - It 'Compare single discrepancies' { - $Obj1 = ConvertFrom-Json ' - { - "NonNodeData": { - "Exchange": { - "AcceptedDomains": [ - { - "DomainType": "Authoritative", - "Ensure": "PresentX", - "MatchSubDomains": true, - "OutboundOnly": true, - "UniqueId": "Default" - } - ], - "ActiveSyncDeviceAccessRules": [], - "AddressBookPolicies": [], - "AddressLists": [] - } - } - }' - $Obj2 = ConvertFrom-Json ' - { - "NonNodeData": { - "Exchange": { - "AcceptedDomains": [ - { - "DomainType": "Authoritative", - "Ensure": "PresentY", - "MatchSubDomains": true, - "OutboundOnly": true, - "UniqueId": "Default" - } - ], - "ActiveSyncDeviceAccessRules": [], - "AddressBookPolicies": [], - "AddressLists": [] - } - } - }' - $Obj1 | Compare-ObjectGraph $Obj2 -IsEqual | Should -Be $False - $Result = $Obj1 | Compare-ObjectGraph $Obj2 - @($Result).Count | Should -Be 1 - $Result[0].Path | Should -Be 'NonNodeData.Exchange.AcceptedDomains[0].Ensure' - $Result[0].Discrepancy | Should -Be 'Value' - $Result[0].InputObject | Should -Be 'PresentX' - $Result[0].Reference | Should -Be 'PresentY' - } - - It "#20 Case insensitive" { - $Object = @{ - Comment = 'SAMPLE ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - $Object | Compare-ObjectGraph $Reference | Should -BeNullOrEmpty - } - - It '#121 Primary Key not being used?' { - - $Reference = [Ordered]@{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - - $Object = [Ordered]@{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - } - @{ - Index = 3 - Name = 'Three' - } - @{ - Index = 2 - Name = 'Two' - } - ) - } - - $Result = $Object | Compare-ObjectGraph $Reference -PrimaryKey Index - $Result | Where-Object { $_.Path.Nodes[-1].Name -eq 'Index' } | Should -BeNullOrEmpty - } - - It "#111 Compare-ObjectGraph fails with certain object graphs, presumably due to cyclical references" { - $Result = ,(Get-Item /) | Compare-ObjectGraph -Reference (Get-Item /) -Depth 5 -WarningAction SilentlyContinue - @($Result).Count | Should -BeLessThan 3 - } - } -} diff --git a/_Temp/Immediately.ps1 b/_Temp/Immediately.ps1 deleted file mode 100644 index b6af2e2..0000000 --- a/_Temp/Immediately.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -function Write-Immediately { - param([int]$Depth = 0, $RefTest) - begin { Write-Output 'Beginning...' } - process { - if ($Depth -gt 0) { - Write-Output "Processing $Depth" - Write-Immediately -Depth ($Depth - 1) -RefTest ([Ref]$Test) - } - Start-Sleep 1 - $RefTest.Value = $RefTest.Value + "$Depth" - } - end { Write-Output 'Ending...' } -} - -$Test = '' -Write-Immediately -Depth 5 -RefTest ([Ref]$Test) -$Test \ No newline at end of file diff --git a/_Temp/ObjectGraphTools.psm1 b/_Temp/ObjectGraphTools.psm1 deleted file mode 100644 index 9427e9d..0000000 --- a/_Temp/ObjectGraphTools.psm1 +++ /dev/null @@ -1,4678 +0,0 @@ -#Region Using - -using namespace System.Collections -using namespace System.Collections.Generic -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Linq.Expressions -using namespace System.Reflection - -#EndRegion Using - -#Region Enum - -enum LogicalOperatorEnum { - not = 0 - and = 1 - or = 2 - xor = 3 -} -enum PSNodeStructure { - Leaf = 0 - List = 1 - Map = 2 -} -enum PSNodeOrigin { - Root = 0 - List = 1 - Map = 2 -} -enum ObjectCompareMode { - Equals = 0 - Compare = 1 - Report = 2 -} -[Flags()] enum ObjectComparison { - MatchCase = 1 - MatchType = 2 - IgnoreListOrder = 4 - MatchMapOrder = 8 - Descending = 128 -} -enum XdnType { - Root = 0 - Ancestor = 1 - Index = 2 - Child = 3 - Descendant = 4 - Offspring = 5 - Equals = 9 - Error = 99 -} -enum XdnColorName { - Reset = 0 - Regular = 1 - Literal = 2 - WildCard = 3 - Operator = 4 - Error = 99 -} - -#EndRegion Enum - -#Region Class - -Class Abbreviate { - hidden static [String]$Ellipses = [Char]0x2026 - - hidden [String] $Prefix - hidden [String] $String - hidden [String] $AndSoForth = [Abbreviate]::Ellipses - hidden [String] $Suffix - hidden [Int] $MaxLength - - Abbreviate([String]$Prefix, [String]$String, [Int]$MaxLength, [String]$AndSoForth, [String]$Suffix) { - $this.Prefix = $Prefix - $this.String = $String - $this.MaxLength = $MaxLength - $this.AndSoForth = $AndSoForth - $this.Suffix = $Suffix - } - Abbreviate([String]$Prefix, [String]$String, [Int]$MaxLength, [String]$Suffix) { - $this.Prefix = $Prefix - $this.String = $String - $this.MaxLength = $MaxLength - $this.Suffix = $Suffix - } - Abbreviate([String]$String, [Int]$MaxLength) { - $this.String = $String - $this.MaxLength = $MaxLength - } - - [String] ToString() { - if ($this.MaxLength -le 0) { return $this.String } - if ($this.String.Length -gt 3 * $this.MaxLength) { $this.String = $this.String.SubString(0, (3 * $this.MaxLength)) } # https://stackoverflow.com/q/78787537/1701026 - $this.String = [Regex]::Replace($this.String, '\s+', ' ') - if ($this.Prefix.Length + $this.String.Length + $this.Suffix.Length -gt $this.MaxLength) { - $Length = $this.MaxLength - $this.Prefix.Length - $this.AndSoForth.Length - $this.Suffix.Length - if ($Length -gt 0) { $this.String = $this.String.SubString(0, $Length) + $this.AndSoForth } else { $this.String = $this.AndSoForth } - } - return $this.Prefix + $this.String + $this.Suffix - } -} -class LogicalTerm {} -Class PSNodePath { - hidden [PSNode[]]$Nodes - hidden [String]$_String - - hidden PSNodePath($Nodes) { $this.Nodes = [PSNode[]]$Nodes } - - static [String] op_Addition([PSNodePath]$Path, [String]$String) { - return "$Path" + $String - } - - [Bool] Equals([Object]$Path) { - if ($Path -is [PSNodePath]) { - if ($this.Nodes.Count -ne $Path.Nodes.Count) { return $false } - $Index = 0 - foreach( $Node in $this.Nodes) { - if ($Node.NodeOrigin -ne $Path.Nodes[$Index].NodeOrigin -or - $Node.Name -ne $Path.Nodes[$Index].Name - ) { return $false } - $Index++ - } - return $true - } - elseif ($Path -is [String]) { - return $this.ToString() -eq $Path - } - return $false - } - - [String] ToString() { - if ($Null -eq $this._String) { - $Count = $this.Nodes.Count - $this._String = if ($Count -gt 1) { $this.Nodes[-2].Path.ToString() } - $Node = $this.Nodes[-1] - $this._String += # Copy the new path into the current node - if ($Node.NodeOrigin -eq 'List') { - "[$($Node._Name)]" - } - elseif ($Node.NodeOrigin -eq 'Map') { - $KeyExpression = [PSKeyExpression]$Node._Name - if ($Count -le 2) { $KeyExpression } else { ".$KeyExpression" } - } - } - return $this._String - } - -} -Class PSNode : IComparable { - hidden static PSNode() { Use-ClassAccessors } - - static [int]$DefaultMaxDepth = 20 - - hidden $_Name - [Int]$Depth - hidden $_Value - hidden [Int]$_MaxDepth = [PSNode]::DefaultMaxDepth - [PSNode]$ParentNode - [PSNode]$RootNode = $this - hidden [Dictionary[String,Object]]$Cache = [Dictionary[String,Object]]::new() - hidden [DateTime]$MaxDepthWarningTime # Warn ones per item branch - - static ExportTypes() { # https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#exporting-classes-with-type-accelerators - # Define the types to export with type accelerators. - $ExportableTypes =@( - [PSNode] - [PSCollectionNode] - [PSListNode] - [PSMapNode] - [PSDictionaryNode] - [PSObjectNode] - ) - # Get the internal TypeAccelerators class to use its static methods. - $TypeAcceleratorsClass = [psobject].Assembly.GetType( - 'System.Management.Automation.TypeAccelerators' - ) - # Ensure none of the types would clobber an existing type accelerator. - # If a type accelerator with the same name exists, throw an exception. - $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get - foreach ($Type in $ExportableTypes) { - if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { - $Message = @( - "Unable to register type accelerator '$($Type.FullName)'" - 'Accelerator already exists.' - ) -join ' - ' - - throw [System.Management.Automation.ErrorRecord]::new( - [System.InvalidOperationException]::new($Message), - 'TypeAcceleratorAlreadyExists', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $Type.FullName - ) - } - } - # Add type accelerators for every exportable type. - foreach ($Type in $ExportableTypes) { - $TypeAcceleratorsClass::Add($Type.FullName, $Type) - } - # Remove type accelerators when the module is removed. - $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { - foreach($Type in $ExportableTypes) { - $TypeAcceleratorsClass::Remove($Type.FullName) - } - }.GetNewClosure() - } - - static [PSNode] ParseInput($Object, $MaxDepth) { - $Node = - if ($Object -is [PSNode]) { $Object } - else { - if ($Null -eq $Object) { [PSLeafNode]::new($Object) } - elseif ($Object -is [Management.Automation.PSCustomObject]) { [PSObjectNode]::new($Object) } - elseif ($Object -is [Collections.IDictionary]) { [PSDictionaryNode]::new($Object) } - elseif ($Object -is [Specialized.StringDictionary]) { [PSDictionaryNode]::new($Object) } - elseif ($Object -is [Collections.ICollection]) { [PSListNode]::new($Object) } - elseif ($Object -is [ValueType]) { [PSLeafNode]::new($Object) } - elseif ($Object -is [String]) { [PSLeafNode]::new($Object) } - elseif ($Object -is [Type]) { [PSLeafNode]::new($Object) } - elseif ($Object -is [ScriptBlock]) { [PSLeafNode]::new($Object) } - elseif ($Object.PSObject.Properties) { [PSObjectNode]::new($Object) } - else { [PSLeafNode]::new($Object) } - } - $Node.RootNode = $Node - if ($MaxDepth -gt 0) { $Node._MaxDepth = $MaxDepth } - return $Node - } - - static [PSNode] ParseInput($Object) { return [PSNode]::parseInput($Object, 0) } - - static [int] Compare($Left, $Right) { - return [ObjectComparer]::new().Compare($Left, $Right) - } - static [int] Compare($Left, $Right, [String[]]$PrimaryKey) { - return [ObjectComparer]::new($PrimaryKey, 0, [CultureInfo]::CurrentCulture).Compare($Left, $Right) - } - static [int] Compare($Left, $Right, [String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { - return [ObjectComparer]::new($PrimaryKey, $ObjectComparison, [CultureInfo]::CurrentCulture).Compare($Left, $Right) - } - static [int] Compare($Left, $Right, [String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison, [CultureInfo]$CultureInfo) { - return [ObjectComparer]::new($PrimaryKey, $ObjectComparison, $CultureInfo).Compare($Left, $Right) - } - - hidden [object] get_Value() { return ,$this._Value } - - hidden set_Value($Value) { - $this.Cache.Remove('ChildNodes') - $this._Value = $Value - if ($Null -ne $this.ParentNode) { $this.ParentNode.SetValue($this._Name, $Value) } - if ($this.GetType() -ne [PSNode]::ParseInput($Value).GetType()) { # The root node is of type PSNode (always false) - Write-Warning "The supplied value has a different PSNode type than the existing $($this.Path). Use .ParentNode.SetValue() method and reload its child item(s)." - } - } - - hidden [Object] get_Name() { return ,$this._Name } - - hidden [Object] get_MaxDepth() { return $this.RootNode._MaxDepth } - - hidden set_MaxDepth($MaxDepth) { - if (-not $this.ChildType) { - $this._MaxDepth = $MaxDepth - } - else { - Throw 'The MaxDepth can only be set at the root node: [PSNode].RootNode.MaxDepth = ' - } - } - - hidden [PSNodeStructure] get_NodeStructure() { - if ($this -is [PSListNode]) { return 'List' } elseif ($this -is [PSMapNode]) { return 'Map' } else { return 'Leaf' } - } - - hidden [PSNodeOrigin] get_NodeOrigin() { - if ($this.ParentNode -is [PSListNode]) { return 'List' } elseif ($this.ParentNode -is [PSMapNode]) { return 'Map' } else { return 'Root' } - } - - hidden [Type] get_ValueType() { - if ($Null -eq $this._Value) { return $Null } - else { return $this._Value.getType() } - } - - [Int]GetHashCode() { return $this.GetHashCode($false) } # Ignore the case of a string value - - hidden [Object] get_Path() { - if (-not $this.Cache.ContainsKey('Path')) { - if ($this.ParentNode) { - $this.Cache['Path'] = [PSNodePath]($this.ParentNode.get_Path().Nodes + $this) - } - else { - $this.Cache['Path'] = [PSNodePath]$this - } - } - return $this.Cache['Path'] - } - - [String] GetPathName($VariableName) { - $PathName = $this.get_Path().ToString() - if (-not $PathName) { return $VariableName } - elseif ($PathName.StartsWith('.')) { return "$VariableName$PathName" } - else { return "$VariableName.$PathName" } - } - - [String] GetPathName() { return $this.get_Path().ToString() } - - hidden [String] get_Expression() { return [PSSerialize]$this } - - Remove() { - if ($null -eq $this.ParentNode) { Throw "The root node can't be removed." } - $this.ParentNode.RemoveAt($this.Name) - $this.Cache.Remove('ChildNodes') - } - - [Bool] Equals($Object) { # https://learn.microsoft.com/dotnet/api/system.globalization.compareoptions - if ($Object -is [PSNode]) { $Node = $Object } - else { $Node = [PSNode]::ParseInput($Object) } - $ObjectComparer = [ObjectComparer]::new() - return $ObjectComparer.IsEqual($this, $Node) - } - - [int] CompareTo($Object) { - if ($Object -is [PSNode]) { $Node = $Object } - else { $Node = [PSNode]::ParseInput($Object) } - $ObjectComparer = [ObjectComparer]::new() - return $ObjectComparer.Compare($this, $Node) - } - - hidden CollectNodes($NodeTable, [XdnPath]$Path, [Int]$PathIndex) { - $Entry = $Path.Entries[$PathIndex] - $NextIndex = if ($PathIndex -lt $Path.Entries.Count -1) { $PathIndex + 1 } - $NextEntry = if ($NextIndex) { $Path.Entries[$NextIndex] } - $Equals = if ($NextEntry -and $NextEntry.Key -eq 'Equals') { - $NextEntry.Value - $NextIndex = if ($NextIndex -lt $Path.Entries.Count -1) { $NextIndex + 1 } - } - switch ($Entry.Key) { - Root { - $Node = $this.RootNode - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - Ancestor { - $Node = $this - for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode } - if ($i -eq 0) { # else: reached root boundary - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - } - Index { - if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) { - $Node = $this.GetChildNode([Int]$Entry.Value) - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - } - Default { # Child, Descendant - if ($this -is [PSListNode]) { # Member access enumeration - foreach ($Node in $this.get_ChildNodes()) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) - } - } - elseif ($this -is [PSMapNode]) { - $Found = $False - $ChildNodes = $this.get_ChildNodes() - foreach ($Node in $ChildNodes) { - if ($Entry.Value -eq $Node.Name -and (-not $Equals -or ($Node -is [PSLeafNode] -and $Equals -eq $Node._Value))) { - $Found = $True - if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) } - else { $NodeTable[$Node.getPathName()] = $Node } - } - } - if (-not $Found -and $Entry.Key -eq 'Descendant') { - foreach ($Node in $ChildNodes) { - $Node.CollectNodes($NodeTable, $Path, $PathIndex) - } - } - } - } - } - } - - [Object] GetNode([XdnPath]$Path) { - $NodeTable = [system.collections.generic.dictionary[String, PSNode]]::new() # Case sensitive (case insensitive map nodes use the same name) - $this.CollectNodes($NodeTable, $Path, 0) - if ($NodeTable.Count -eq 0) { return @() } - if ($NodeTable.Count -eq 1) { return $NodeTable[$NodeTable.Keys] } - else { return [PSNode[]]$NodeTable.Values } - } -} -class ObjectComparer { - - # Report properties (column names) - [String]$Name1 = 'Reference' - [String]$Name2 = 'InputObject' - [String]$Issue = 'Discrepancy' - - [String[]]$PrimaryKey - [ObjectComparison]$ObjectComparison - - [Collections.Generic.List[Object]]$Differences - - ObjectComparer () {} - ObjectComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - ObjectComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - ObjectComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - ObjectComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - - [bool] IsEqual ($Object1, $Object2) { return $this.Compare($Object1, $Object2, 'Equals') } - [int] Compare ($Object1, $Object2) { return $this.Compare($Object1, $Object2, 'Compare') } - [Object] Report ($Object1, $Object2) { - $this.Differences = [Collections.Generic.List[Object]]::new() - $null = $this.Compare($Object1, $Object2, 'Report') - return $this.Differences - } - - [Object] Compare($Object1, $Object2, [ObjectCompareMode]$Mode) { - if ($Object1 -is [PSNode]) { $Node1 = $Object1 } else { $Node1 = [PSNode]::ParseInput($Object1) } - if ($Object2 -is [PSNode]) { $Node2 = $Object2 } else { $Node2 = [PSNode]::ParseInput($Object2) } - return $this.CompareRecurse($Node1, $Node2, $Mode) - } - - hidden [Object] CompareRecurse([PSNode]$Node1, [PSNode]$Node2, [ObjectCompareMode]$Mode) { - $Comparison = $this.ObjectComparison - $MatchCase = $Comparison -band 'MatchCase' - $EqualType = $true - if ($Mode -ne 'Compare') { - if ($MatchCase -and $Node1.ValueType -ne $Node2.ValueType) { - if ($Mode -eq 'Equals') { return $false } else { # if ($Mode -eq 'Report') - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Type' - $this.Name1 = $Node1.ValueType - $this.Name2 = $Node2.ValueType - }) - } - } - if ($Node1 -is [PSCollectionNode] -and $Node2 -is [PSCollectionNode] -and $Node1.Count -ne $Node2.Count) { - if ($Mode -eq 'Equals') { return $false } else { # if ($Mode -eq 'Report') - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Size' - $this.Name1 = $Node1.Count - $this.Name2 = $Node2.Count - }) - } - } - } - - if ($Node1 -is [PSLeafNode] -and $Node2 -is [PSLeafNode]) { - $Eq = if ($MatchCase) { $Node1.Value -ceq $Node2.Value } else { $Node1.Value -eq $Node2.Value } - Switch ($Mode) { - Equals { return $Eq } - Compare { - if ($Eq) { return 1 - $EqualType } # different types results in 1 (-gt) - else { - $Greater = if ($MatchCase) { $Node1.Value -cgt $Node2.Value } else { $Node1.Value -gt $Node2.Value } - if ($Greater -xor $Comparison -band 'Descending') { return 1 } else { return -1 } - } - } - default { - if (-not $Eq) { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path - $this.Issue = 'Value' - $this.Name1 = $Node1.Value - $this.Name2 = $Node2.Value - }) - } - } - } - } - elseif ($Node1 -is [PSListNode] -and $Node2 -is [PSListNode]) { - $MatchOrder = -not ($Comparison -band 'IgnoreListOrder') - # if ($Node1.GetHashCode($MatchCase) -eq $Node2.GetHashCode($MatchCase)) { - # if ($Mode -eq 'Equals') { return $true } else { return 0 } # Report mode doesn't care about the output - # } - $Items1 = $Node1.ChildNodes - $Items2 = $Node2.ChildNodes - if (-not $Items1 -and -not $Items2) { - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - if ($Items1.Count) { $Indices1 = [Collections.Generic.List[Int]]$Items1.Name } else { $Indices1 = @() } - if ($Items2.Count) { $Indices2 = [Collections.Generic.List[Int]]$Items2.Name } else { $Indices2 = @() } - if ($this.PrimaryKey) { - $Maps2 = [Collections.Generic.List[Int]]$Items2.where{ $_ -is [PSMapNode] }.Name - if ($Maps2.Count) { - $Maps1 = [Collections.Generic.List[Int]]$Items1.where{ $_ -is [PSMapNode] }.Name - if ($Maps1.Count) { - foreach ($Key in $this.PrimaryKey) { - foreach($Index2 in @($Maps2)) { - $Item2 = $Items2[$Index2] - foreach ($Index1 in @($Maps1)) { - $Item1 = $Items1[$Index1] - if ($Item1.GetValue($Key) -eq $Item2.GetValue($Key)) { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - Switch ($Mode) { - Equals { if (-not $Compare) { return $Compare } } - Compare { if ($Compare) { return $Compare } } - } - $Null = $Indices1.Remove($Index1) - $null = $Indices2.Remove($Index2) - $Null = $Maps1.Remove($Index1) - $null = $Maps2.Remove($Index2) - break # Only match the first primary key - } - } - } - } - # in case of any single maps leftover without primary keys - if($Maps2.Count -eq 1 -and $Maps1.Count -eq 1) { Write-Host - $Item1 = $Items1[$Maps1[0]] - $Item2 = $Items2[$Maps2[0]] - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - Switch ($Mode) { - Equals { if (-not $Compare) { return $Compare } } - Compare { if ($Compare) { return $Compare } } - } - $null = $Indices2.Remove($Maps2[0]) - $Null = $Indices1.Remove($Maps1[0]) - $Maps2.Clear() - $Maps1.Clear() - } - } - } - } - if (-not $MatchOrder) { # remove the equal nodes from the lists - foreach($Index2 in @($Indices2)) { - $Item2 = $Items2[$Index2] - foreach ($Index1 in $Indices1) { - $Item1 = $Items1[$Index1] - if ($this.CompareRecurse($Item1, $Item2, 'Equals')) { - $null = $Indices2.Remove($Index2) - $Null = $Indices1.Remove($Index1) - break # Only match a single node - } - } - } - } - for ($i = 0; $i -lt [math]::max($Indices2.Count, $Indices1.Count); $i++) { - $Index1 = if ($i -lt $Indices1.Count) { $Indices1[$i] } - $Index2 = if ($i -lt $Indices2.Count) { $Indices2[$i] } - $Item1 = if ($Null -ne $Index1) { $Items1[$Index1] } - $Item2 = if ($Null -ne $Index2) { $Items2[$Index2] } - if ($Null -eq $Item1) { - Switch ($Mode) { - Equals { return $false } - Compare { return -1 } # None existing items can't be ordered - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node2.Path + "[$Index2]" - $this.Issue = 'Exists' - $this.Name1 = $Null - $this.Name2 = if ($Item2 -is [PSLeafNode]) { "$($Item2.Value)" } else { "[$($Item2.ValueType)]" } - }) - } - } - } - elseif ($Null -eq $Item2) { - Switch ($Mode) { - Equals { return $false } - Compare { return 1 } # None existing items can't be ordered - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.Path + "[$Index1]" - $this.Issue = 'Exists' - $this.Name1 = if ($Item1 -is [PSLeafNode]) { "$($Item1.Value)" } else { "[$($Item1.ValueType)]" } - $this.Name2 = $Null - }) - } - } - } - else { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - } - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - elseif ($Node1 -is [PSMapNode] -and $Node2 -is [PSMapNode]) { - $Items1 = $Node1.ChildNodes - $Items2 = $Node2.ChildNodes - if (-not $Items1 -and -not $Items2) { - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - $MatchOrder = [Bool]($Comparison -band 'MatchMapOrder') - if ($MatchOrder -and $Node1._Value -isnot [HashTable] -and $Node2._Value -isnot [HashTable]) { - $Index = 0 - foreach ($Item1 in $Items1) { - if ($Index -lt $Items2.Count) { $Item2 = $Items2[$Index++] } else { break } - $EqualName = if ($MatchCase) { $Item1.Name -ceq $Item2.Name } else { $Item1.Name -eq $Item2.Name } - if ($EqualName) { - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - else { - Switch ($Mode) { - Equals { return $false } - Compare {} # The order depends on the child name and value - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Item1.Path - $this.Issue = 'Name' - $this.Name1 = $Item1.Name - $this.Name2 = $Item2.Name - }) - } - } - } - } - } - else { - $Found = [HashTable]::new() # (Case sensitive) - foreach ($Item2 in $Items2) { - if ($Node1.Contains($Item2.Name)) { - $Item1 = $Node1.GetChildNode($Item2.Name) # Left defines the comparer - $Found[$Item1.Name] = $true - $Compare = $this.CompareRecurse($Item1, $Item2, $Mode) - if (($Mode -eq 'Equals' -and -not $Compare) -or ($Mode -eq 'Compare' -and $Compare)) { return $Compare } - } - else { - Switch ($Mode) { - Equals { return $false } - Compare { return -1 } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Item2.Path - $this.Issue = 'Exists' - $this.Name1 = $false - $this.Name2 = $true - }) - } - } - } - } - foreach ($Name in $Node1.Names) { - if (-not $Found.Contains($Name)) { - Switch ($Mode) { - Equals { return $false } - Compare { return 1 } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.GetChildNode($Name).Path - $this.Issue = 'Exists' - $this.Name1 = $true - $this.Name2 = $false - }) - } - } - } - } - } - if ($Mode -eq 'Equals') { return $true } elseif ($Mode -eq 'Compare') { return 0 } else { return @() } - } - else { # Different structure - Switch ($Mode) { - Equals { return $false } - Compare { # Structure order: PSLeafNode - PSListNode - PSMapNode (can't be reversed) - if ($Node1 -is [PSLeafNode] -or $Node2 -isnot [PSMapNode] ) { return -1 } else { return 1 } - } - default { - $this.Differences.Add([PSCustomObject]@{ - Path = $Node1.Path - $this.Issue = 'Structure' - $this.Name1 = $Node1.ValueType.Name - $this.Name2 = $Node2.ValueType.Name - }) - } - } - } - if ($Mode -eq 'Equals') { throw 'Equals comparison should have returned boolean.' } - if ($Mode -eq 'Compare') { throw 'Compare comparison should have returned integer.' } - return @() - } -} -class PSMapNodeComparer : IComparer[Object] { - [String[]]$PrimaryKey - [ObjectComparison]$ObjectComparison - - PSMapNodeComparer () {} - PSMapNodeComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - PSMapNodeComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - PSMapNodeComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - PSMapNodeComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - [int] Compare ([Object]$Node1, [Object]$Node2) { - $Comparison = $this.ObjectComparison - $MatchCase = $Comparison -band 'MatchCase' - $Equal = if ($MatchCase) { $Node1.Name -ceq $Node2.Name } else { $Node1.Name -eq $Node2.Name } - if ($Equal) { return 0 } - else { - if ($this.PrimaryKey) { # Primary keys take always priority - if ($this.PrimaryKey -eq $Node1.Name) { return -1 } - if ($this.PrimaryKey -eq $Node2.Name) { return 1 } - } - $Greater = if ($MatchCase) { $Node1.Name -cgt $Node2.Name } else { $Node1.Name -gt $Node2.Name } - if ($Greater -xor $Comparison -band 'Descending') { return 1 } else { return -1 } - } - } -} -Class PSDeserialize { - hidden static [String[]]$Parameters = 'LanguageMode', 'ArrayType', 'HashTableType' - hidden static PSDeserialize() { Use-ClassAccessors } - - hidden $_Object - [PSLanguageMode]$LanguageMode = 'Restricted' - [Type]$ArrayType = 'Array' -as [Type] - [Type]$HashTableType = 'HashTable' -as [Type] - [String] $Expression - - PSDeserialize([String]$Expression) { $this.Expression = $Expression } - PSDeserialize( - $Expression, - $LanguageMode = 'Restricted', - $ArrayType = $Null, - $HashTableType = $Null - ) { - if ($this.LanguageMode -eq 'NoLanguage') { # No language mode is internally used for displaying - Throw 'The language mode "NoLanguage" is not supported.' - } - $this.Expression = $Expression - $this.LanguageMode = $LanguageMode - if ($Null -ne $ArrayType) { $this.ArrayType = $ArrayType } - if ($Null -ne $HashTableType) { $this.HashTableType = $HashTableType } - } - - hidden [Object] get_Object() { - if ($Null -eq $this._Object) { - $Ast = [System.Management.Automation.Language.Parser]::ParseInput($this.Expression, [ref]$null, [ref]$Null) - $this._Object = $this.ParseAst([Ast]$Ast) - } - return $this._Object - } - - hidden [Object] ParseAst([Ast]$Ast) { - # Write-Host 'Ast type:' "$($Ast.getType())" - $Type = $Null - if ($Ast -is [ConvertExpressionAst]) { - $FullTypeName = $Ast.Type.TypeName.FullName - if ( - $this.LanguageMode -eq 'Full' -or ( - $this.LanguageMode -eq 'Constrained' -and - [PSLanguageType]::IsConstrained($FullTypeName) - ) - ) { - try { $Type = $FullTypeName -as [Type] } catch { write-error $_ } - } - $Ast = $Ast.Child - } - if ($Ast -is [ScriptBlockAst]) { - $List = [List[Object]]::new() - if ($Null -ne $Ast.BeginBlock) { $Ast.BeginBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($Null -ne $Ast.ProcessBlock) { $Ast.ProcessBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($Null -ne $Ast.EndBlock) { $Ast.EndBlock.Statements.ForEach{ $List.Add($this.ParseAst($_)) } } - if ($List.Count -eq 1) { return $List[0] } else { return @($List) } - } - elseif ($Ast -is [PipelineAst]) { - $Elements = $Ast.PipelineElements - if (-not $Elements.Count) { return @() } - elseif ($Elements -is [CommandAst]) { - return $Null #85 ConvertFrom-Expression: convert function/cmdlet calls to Objects - } - elseif ($Elements.Expression.Count -eq 1) { return $this.ParseAst($Elements.Expression[0]) } - else { return $Elements.Expression.Foreach{ $this.ParseAst($_) } } - } - elseif ($Ast -is [ArrayLiteralAst] -or $Ast -is [ArrayExpressionAst]) { - if (-not $Type -or 'System.Object[]', 'System.Array' -eq $Type.FullName) { $Type = $this.ArrayType } - if ($Ast -is [ArrayLiteralAst]) { $Value = $Ast.Elements.foreach{ $this.ParseAst($_) } } - else { $Value = $Ast.SubExpression.Statements.foreach{ $this.ParseAst($_) } } - if ('System.Object[]', 'System.Array' -eq $Type.FullName) { - if ($Value -isnot [Array]) { $Value = @($Value) } # Prevent single item array unrolls - } - else { $Value = $Value -as $Type } - return $Value - } - elseif ($Ast -is [HashtableAst]) { - if (-not $Type -or $Type.FullName -eq 'System.Collections.Hashtable') { $Type = $this.HashTableType } - $IsPSCustomObject = "$Type" -in - 'PSCustomObject', - 'System.Management.Automation.PSCustomObject', - 'PSObject', - 'System.Management.Automation.PSObject' - if ($Type.FullName -eq 'System.Collections.Hashtable') { $Map = @{} } # Case insensitive - elseif ($IsPSCustomObject) { $Map = [Ordered]@{} } - else { $Map = New-Object -Type $Type } - $Ast.KeyValuePairs.foreach{ - if ( $Map -is [Collections.IDictionary]) { $Map.Add($_.Item1.Value, $this.ParseAst($_.Item2)) } - else { $Map."$($_.Item1.Value)" = $this.ParseAst($_.Item2) } - } - if ($IsPSCustomObject) { return [PSCustomObject]$Map } else { return $Map } - } - elseif ($Ast -is [ConstantExpressionAst]) { - if ($Type) { $Value = $Ast.Value -as $Type } else { $Value = $Ast.Value } - return $Value - } - elseif ($Ast -is [VariableExpressionAst]) { - $Value = switch ($Ast.VariablePath.UserPath) { - Null { $Null } - True { $True } - False { $False } - PSCulture { (Get-Culture).ToString() } - PSUICulture { (Get-UICulture).ToString() } - Default { $Ast.Extent.Text } - } - return $Value - } - else { return $Null } - } -} -Class PSInstance { - static [Object]Create($Object) { - if ($Null -eq $Object) { return $Null } - elseif ($Object -is [String]) { - $String = if ($Object.StartsWith('[') -and $Object.EndsWith(']')) { $Object.SubString(1, ($Object.Length - 2)) } else { $Object } - Switch -Regex ($String) { - '^((System\.)?String)?$' { return '' } - '^(System\.)?Array$' { return ,@() } - '^(System\.)?Object\[\]$' { return ,@() } - '^((System\.)?Collections\.Hashtable\.)?hashtable$' { return @{} } - '^((System\.)?Management\.Automation\.)?ScriptBlock$' { return {} } - '^((System\.)?Collections\.Specialized\.)?Ordered(Dictionary)?$' { return [Ordered]@{} } - '^((System\.)?Management\.Automation\.)?PS(Custom)?Object$' { return [PSCustomObject]@{} } - } - $Type = $String -as [Type] - if (-not $Type) { Throw "Unknown type: [$Object]" } - } - elseif ($Object -is [Type]) { - $Type = $Object.UnderlyingSystemType - if ("$Type" -eq 'string') { Return '' } - elseif ("$Type" -eq 'array') { Return ,@() } - elseif ("$Type" -eq 'scriptblock') { Return {} } - } - else { - if ($Object -is [Object[]]) { Return ,@() } - elseif ($Object -is [ScriptBlock]) { Return {} } - elseif ($Object -is [PSCustomObject]) { Return [PSCustomObject]::new() } - $Type = $Object.GetType() - } - try { return [Activator]::CreateInstance($Type) } catch { throw $_ } - } -} -Class PSKeyExpression { - hidden static [Regex]$UnquoteMatch = '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices - hidden $Key - hidden [PSLanguageMode]$LanguageMode = 'Restricted' - hidden [Bool]$Compress - hidden [Int]$MaxLength - - PSKeyExpression($Key) { $this.Key = $Key } - PSKeyExpression($Key, [PSLanguageMode]$LanguageMode) { $this.Key = $Key; $this.LanguageMode = $LanguageMode } - PSKeyExpression($Key, [PSLanguageMode]$LanguageMode, [Bool]$Compress) { $this.Key = $Key; $this.LanguageMode = $LanguageMode; $this.Compress = $Compress } - PSKeyExpression($Key, [int]$MaxLength) { $this.Key = $Key; $this.MaxLength = $MaxLength } - - [String]ToString() { - $Name = $this.Key - if ($Name -is [byte] -or $Name -is [int16] -or $Name -is [int32] -or $Name -is [int64] -or - $Name -is [sByte] -or $Name -is [uint16] -or $Name -is [uint32] -or $Name -is [uint64] -or - $Name -is [float] -or $Name -is [double] -or $Name -is [decimal]) { return [Abbreviate]::new($Name, $this.MaxLength) - } - if ($this.MaxLength) { $Name = "$Name" } - if ($Name -is [String]) { - if ($Name -cMatch [PSKeyExpression]::UnquoteMatch) { return [Abbreviate]::new($Name, $this.MaxLength) } - return "'$([Abbreviate]::new($Name.Replace("'", "''"), ($this.MaxLength - 2)))'" - } - $Node = [PSNode]::ParseInput($Name, 2) # There is no way to expand keys more than 2 levels - return [PSSerialize]::new($Node, $this.LanguageMode, -$this.Compress) - } -} -Class PSLanguageType { - hidden static $_TypeCache = [Dictionary[String,Bool]]::new() - hidden Static PSLanguageType() { # Hardcoded - [PSLanguageType]::_TypeCache['System.Void'] = $True - [PSLanguageType]::_TypeCache['System.Management.Automation.PSCustomObject'] = $True # https://github.com/PowerShell/PowerShell/issues/20767 - } - static [Bool]IsRestricted($TypeName) { - if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a restricted "type"! - $Type = $TypeName -as [Type] - if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } - $TypeName = $Type.FullName - return $TypeName -in 'bool', 'array', 'hashtable' - } - static [Bool]IsConstrained($TypeName) { # https://stackoverflow.com/a/64806919/1701026 - if ($Null -eq $TypeName) { return $True } # Warning: a $Null is considered a constrained "type"! - $Type = $TypeName -as [Type] - if ($Null -eq $Type) { Throw 'Unknown type name: $TypeName' } - $TypeName = $Type.FullName - if (-not [PSLanguageType]::_TypeCache.ContainsKey($TypeName)) { - [PSLanguageType]::_TypeCache[$TypeName] = try { - $ConstrainedSession = [PowerShell]::Create() - $ConstrainedSession.RunSpace.SessionStateProxy.LanguageMode = 'Constrained' - $ConstrainedSession.AddScript("[$TypeName]0").Invoke().Count -ne 0 -or - $ConstrainedSession.Streams.Error[0].FullyQualifiedErrorId -ne 'ConversionSupportedOnlyToCoreTypes' - } catch { $False } - } - return [PSLanguageType]::_TypeCache[$TypeName] - } -} -Class PSSerialize { - # hidden static [Dictionary[String,Bool]]$IsConstrainedType = [Dictionary[String,Bool]]::new() - hidden static [Dictionary[String,Bool]]$HasStringConstructor = [Dictionary[String,Bool]]::new() - - hidden static [String]$AnySingleQuote = "'|$([char]0x2018)|$([char]0x2019)" - - # NoLanguage mode only - hidden static [int]$MaxLeafLength = 48 - hidden static [int]$MaxKeyLength = 12 - hidden static [int]$MaxValueLength = 16 - hidden static [int[]]$NoLanguageIndices = 0, 1, -1 - hidden static [int[]]$NoLanguageItems = 0, 1, -1 - - hidden $_Object - - hidden [PSLanguageMode]$LanguageMode = 'Restricted' # "NoLanguage" will stringify the object for displaying (Use: PSStringify) - hidden [Int]$ExpandDepth = [Int]::MaxValue - hidden [Bool]$Explicit - hidden [Bool]$FullTypeName - hidden [bool]$HighFidelity - hidden [String]$Indent = ' ' - hidden [Bool]$ExpandSingleton - - # The dictionary below defines the round trip property. Unless the `-HighFidelity` switch is set, - # the serialization will stop (even it concerns a `PSCollectionNode`) when the specific property - # type is reached. - # * An empty string will return the string representation of the object: `""` - # * Any other string will return the string representation of the object property: `"$(.)"` - # * A ScriptBlock will be invoked and the result will be used for the object value - - hidden static $RoundTripProperty = @{ - 'Microsoft.Management.Infrastructure.CimInstance' = '' - 'Microsoft.Management.Infrastructure.CimSession' = 'ComputerName' - 'Microsoft.PowerShell.Commands.ModuleSpecification' = 'Name' - 'System.DateTime' = { $($Input).ToString('o') } - 'System.DirectoryServices.DirectoryEntry' = 'Path' - 'System.DirectoryServices.DirectorySearcher' = 'Filter' - 'System.Globalization.CultureInfo' = 'Name' - 'Microsoft.PowerShell.VistaCultureInfo' = 'Name' - 'System.Management.Automation.AliasAttribute' = 'AliasNames' - 'System.Management.Automation.ArgumentCompleterAttribute' = 'ScriptBlock' - 'System.Management.Automation.ConfirmImpact' = '' - 'System.Management.Automation.DSCResourceRunAsCredential' = '' - 'System.Management.Automation.ExperimentAction' = '' - 'System.Management.Automation.OutputTypeAttribute' = 'Type' - 'System.Management.Automation.PSCredential' = { ,@($($Input).UserName, @("(""$($($Input).Password | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)')) } - 'System.Management.Automation.PSListModifier' = 'Replace' - 'System.Management.Automation.PSReference' = 'Value' - 'System.Management.Automation.PSTypeNameAttribute' = 'PSTypeName' - 'System.Management.Automation.RemotingCapability' = '' - 'System.Management.Automation.ScriptBlock' = 'Ast' - 'System.Management.Automation.SemanticVersion' = '' - 'System.Management.Automation.ValidatePatternAttribute' = 'RegexPattern' - 'System.Management.Automation.ValidateScriptAttribute' = 'ScriptBlock' - 'System.Management.Automation.ValidateSetAttribute' = 'ValidValues' - 'System.Management.Automation.WildcardPattern' = { $($Input).ToWql().Replace('%', '*').Replace('_', '?').Replace('[*]', '%').Replace('[?]', '_') } - 'Microsoft.Management.Infrastructure.CimType' = '' - 'System.Management.ManagementClass' = 'Path' - 'System.Management.ManagementObject' = 'Path' - 'System.Management.ManagementObjectSearcher' = { $($Input).Query.QueryString } - 'System.Net.IPAddress' = 'IPAddressToString' - 'System.Net.IPEndPoint' = { $($Input).Address.Address; $($Input).Port } - 'System.Net.Mail.MailAddress' = 'Address' - 'System.Net.NetworkInformation.PhysicalAddress' = '' - 'System.Security.Cryptography.X509Certificates.X500DistinguishedName' = 'Name' - 'System.Security.SecureString' = { ,[string[]]("(""$($Input | ConvertFrom-SecureString)""", '|', 'ConvertTo-SecureString)') } - 'System.Text.RegularExpressions.Regex' = '' - 'System.RuntimeType' = '' - 'System.Uri' = 'OriginalString' - 'System.Version' = '' - 'System.Void' = $Null - } - hidden $StringBuilder - hidden [Int]$Offset = 0 - hidden [Int]$LineNumber = 1 - - PSSerialize($Object) { $this._Object = $Object } - PSSerialize($Object, $LanguageMode) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode - } - PSSerialize($Object, $LanguageMode, $ExpandDepth) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode - $this.ExpandDepth = $ExpandDepth - } - PSSerialize( - $Object, - $LanguageMode = 'Restricted', - $ExpandDepth = [Int]::MaxValue, - $Explicit = $False, - $FullTypeName = $False, - $HighFidelity = $False, - $ExpandSingleton = $False, - $Indent = ' ' - ) { - $this._Object = $Object - $this.LanguageMode = $LanguageMode - $this.ExpandDepth = $ExpandDepth - $this.Explicit = $Explicit - $this.FullTypeName = $FullTypeName - $this.HighFidelity = $HighFidelity - $this.ExpandSingleton = $ExpandSingleton - $this.Indent = $Indent - } - - hidden static [String[]]$Parameters = 'LanguageMode', 'Explicit', 'FullTypeName', 'HighFidelity', 'Indent', 'ExpandSingleton' - PSSerialize($Object, [HashTable]$Parameters) { - $this._Object = $Object - foreach ($Name in $Parameters.get_Keys()) { # https://github.com/PowerShell/PowerShell/issues/13307 - if ($Name -notin [PSSerialize]::Parameters) { Throw "Unknown parameter: $Name." } - $this.GetType().GetProperty($Name).SetValue($this, $Parameters[$Name]) - } - } - - [String]Serialize($Object) { - if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } - if (-not ('ConstrainedLanguage', 'FullLanguage' -eq $this.LanguageMode)) { - if ($this.FullTypeName) { Write-Warning 'The FullTypeName switch requires Constrained - or FullLanguage mode.' } - if ($this.Explicit) { Write-Warning 'The Explicit switch requires Constrained - or FullLanguage mode.' } - } - if ($Object -is [PSNode]) { $Node = $Object } else { $Node = [PSNode]::ParseInput($Object) } - $this.StringBuilder = [System.Text.StringBuilder]::new() - $this.Stringify($Node) - return $this.StringBuilder.ToString() - } - - hidden Stringify([PSNode]$Node) { - $Value = $Node.Value - $IsSubNode = $this.StringBuilder.Length -ne 0 - if ($Null -eq $Value) { - $this.StringBuilder.Append('$Null') - return - } - $Type = $Node.ValueType - $TypeName = "$Type" - $TypeInitializer = - if ($Null -ne $Type -and ( - $this.LanguageMode -eq 'Full' -or ( - $this.LanguageMode -eq 'Constrained' -and - [PSLanguageType]::IsConstrained($Type) -and ( - $this.Explicit -or -not ( - $Type.IsPrimitive -or - $Value -is [String] -or - $Value -is [Object[]] -or - $Value -is [Hashtable] - ) - ) - ) - ) - ) { - if ($this.FullTypeName) { - if ($Type.FullName -eq 'System.Management.Automation.PSCustomObject' ) { '[System.Management.Automation.PSObject]' } # https://github.com/PowerShell/PowerShell/issues/2295 - else { "[$($Type.FullName)]" } - } - elseif ($TypeName -eq 'System.Object[]') { "[Array]" } - elseif ($TypeName -eq 'System.Management.Automation.PSCustomObject') { "[PSCustomObject]" } - elseif ($Type.Name -eq 'RuntimeType') { "[Type]" } - else { "[$TypeName]" } - } - if ($TypeInitializer) { $this.StringBuilder.Append($TypeInitializer) } - - if ($Node -is [PSLeafNode] -or (-not $this.HighFidelity -and [PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName))) { - $MaxLength = if ($IsSubNode) { [PSSerialize]::MaxValueLength } else { [PSSerialize]::MaxLeafLength } - - if ([PSSerialize]::RoundTripProperty.Contains($Node.ValueType.FullName)) { - $Property = [PSSerialize]::RoundTripProperty[$Node.ValueType.FullName] - if ($Null -eq $Property) { $Expression = $Null } - elseif ($Property -is [String]) { $Expression = if ($Property) { ,$Value.$Property } else { "$Value" } } - elseif ($Property -is [ScriptBlock] ) { $Expression = Invoke-Command $Property -InputObject $Value } - elseif ($Property -is [HashTable]) { $Expression = if ($this.LanguageMode -eq 'Restricted') { $Null } else { @{} } } - elseif ($Property -is [Array]) { $Expression = @($Property.foreach{ $Value.$_ }) } - else { Throw "Unknown round trip property type: $($Property.GetType())."} - } - elseif ($Value -is [Type]) { $Expression = @() } - elseif ($Value -is [Attribute]) { $Expression = @() } - elseif ($Type.IsPrimitive) { $Expression = $Value } - elseif (-not $Type.GetConstructors()) { $Expression = "$TypeName" } - elseif ($Type.GetMethod('ToString', [Type[]]@())) { $Expression = $Value.ToString() } - elseif ($Value -is [Collections.ICollection]) { $Expression = ,$Value } - else { $Expression = $Value } # Handle compression - - if ($Null -eq $Expression) { $Expression = '$Null' } - elseif ($Expression -is [Bool]) { $Expression = "`$$Value" } - elseif ($Expression -is [Char]) { $Expression = "'$Value'" } - elseif ($Expression -is [ScriptBlock]) { $Expression = [Abbreviate]::new('{', $Expression, $MaxLength, '}') } - elseif ($Expression -is [HashTable]) { $Expression = '@{}' } - elseif ($Expression -is [Array]) { - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [Abbreviate]::new('[', $Expression[0], $MaxLength, ']') } - else { - $Space = if ($this.ExpandDepth -ge 0) { ' ' } - $New = if ($TypeInitializer) { '::new(' } else { '@(' } - $Expression = $New + ($Expression.foreach{ - if ($Null -eq $_) { '$Null' } - elseif ($_.GetType().IsPrimitive) { "$_" } - elseif ($_ -is [Array]) { $_ -Join $Space } - else { "'$_'" } - } -Join ",$Space") + ')' - } - } - elseif ($Type -and $Type.IsPrimitive) { - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [CommandColor]([String]$Expression[0]) } - } - else { - if ($Expression -isnot [String]) { $Expression = "$Expression" } - if ($this.LanguageMode -eq 'NoLanguage') { $Expression = [StringColor]([Abbreviate]::new("'", $Expression, $MaxLength, "'")) } - else { - if ($Expression.Contains("`n")) { - $Expression = "@'" + [Environment]::NewLine + "$Expression".Replace("'", "''") + [Environment]::NewLine + "'@" - } - else { $Expression = "'$($Expression -Replace [PSSerialize]::AnySingleQuote, '$0$0')'" } - } - } - - $this.StringBuilder.Append($Expression) - } - elseif ($Node -is [PSListNode]) { - $ChildNodes = $Node.get_ChildNodes() - $this.StringBuilder.Append('@(') - if ($this.LanguageMode -eq 'NoLanguage') { - if ($ChildNodes.Count -eq 0) { } - elseif ($IsSubNode) { $this.StringBuilder.Append([Abbreviate]::Ellipses) } - else { - $Indices = [PSSerialize]::NoLanguageIndices - if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } - $LastIndex = $Null - foreach ($Index in $Indices) { - if ($Null -ne $LastIndex) { $this.StringBuilder.Append(',') } - if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } - if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses),") } - $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) - $LastIndex = $Index - } - } - } - else { - $this.Offset++ - $StartLine = $this.LineNumber - $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or ($ChildNodes.Count -eq 1 -and $ChildNodes[0] -isnot [PSLeafNode]) - foreach ($ChildNode in $ChildNodes) { - if ($ChildNode.Name -gt 0) { - $this.StringBuilder.Append(',') - $this.NewWord() - } - elseif ($ExpandSingle) { $this.NewWord('') } - $this.Stringify($ChildNode) - } - $this.Offset-- - if ($this.LineNumber -gt $StartLine) { $this.NewWord('') } - } - $this.StringBuilder.Append(')') - } - else { # if ($Node -is [PSMapNode]) { - $ChildNodes = $Node.get_ChildNodes() - if ($ChildNodes) { - $this.StringBuilder.Append('@{') - if ($this.LanguageMode -eq 'NoLanguage') { - if ($ChildNodes.Count -gt 0) { - $Indices = [PSSerialize]::NoLanguageItems - if (-not $Indices -or $ChildNodes.Count -lt $Indices.Count) { $Indices = 0..($ChildNodes.Count - 1) } - $LastIndex = $Null - foreach ($Index in $Indices) { - if ($IsSubNode -and $Index) { $this.StringBuilder.Append(";$([Abbreviate]::Ellipses)"); break } - if ($Null -ne $LastIndex) { $this.StringBuilder.Append(';') } - if ($Index -lt 0) { $Index = $ChildNodes.Count + $Index } - if ($Index -gt $LastIndex + 1) { $this.StringBuilder.Append("$([Abbreviate]::Ellipses);") } - $this.StringBuilder.Append([VariableColor]( - [PSKeyExpression]::new($ChildNodes[$Index].Name, [PSSerialize]::MaxKeyLength))) - $this.StringBuilder.Append('=') - if (-not $IsSubNode -or $this.StringBuilder.Length -le [PSSerialize]::MaxKeyLength) { - $this.StringBuilder.Append($this.Stringify($ChildNodes[$Index])) - } - else { $this.StringBuilder.Append([Abbreviate]::Ellipses) } - $LastIndex = $Index - } - } - } - else { - $this.Offset++ - $StartLine = $this.LineNumber - $Index = 0 - $ExpandSingle = $this.ExpandSingleton -or $ChildNodes.Count -gt 1 -or $ChildNodes[0] -isnot [PSLeafNode] - foreach ($ChildNode in $ChildNodes) { - if ($ChildNode.Name -eq 'TypeId' -and $Node._Value -is $ChildNode._Value) { continue } - if ($Index++) { - $Separator = if ($this.ExpandDepth -ge 0) { '; ' } else { ';' } - $this.NewWord($Separator) - } - elseif ($this.ExpandDepth -ge 0) { - if ($ExpandSingle) { $this.NewWord() } else { $this.StringBuilder.Append(' ') } - } - $this.StringBuilder.Append([PSKeyExpression]::new($ChildNode.Name, $this.LanguageMode, ($this.ExpandDepth -lt 0))) - if ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' = ') } else { $this.StringBuilder.Append('=') } - $this.Stringify($ChildNode) - } - $this.Offset-- - if ($this.LineNumber -gt $StartLine) { $this.NewWord() } - elseif ($this.ExpandDepth -ge 0) { $this.StringBuilder.Append(' ') } - } - $this.StringBuilder.Append('}') - } - elseif ($Node -is [PSObjectNode] -and $TypeInitializer) { $this.StringBuilder.Append('::new()') } - else { $this.StringBuilder.Append('@{}') } - } - } - - hidden NewWord() { $this.NewWord(' ') } - hidden NewWord([String]$Separator) { - if ($this.Offset -le $this.ExpandDepth) { - $this.StringBuilder.AppendLine() - for($i = $this.Offset; $i -gt 0; $i--) { - $this.StringBuilder.Append($this.Indent) - } - $this.LineNumber++ - } - else { - $this.StringBuilder.Append($Separator) - } - } - - [String] ToString() { - if ($this._Object -is [PSNode]) { $Node = $this._Object } - else { $Node = [PSNode]::ParseInput($this._Object) } - $this.StringBuilder = [System.Text.StringBuilder]::new() - $this.Stringify($Node) - return $this.StringBuilder.ToString() - } -} -Class ANSI { - # Retrieved from Get-PSReadLineOption - static [String]$CommandColor - static [String]$CommentColor - static [String]$ContinuationPromptColor - static [String]$DefaultTokenColor - static [String]$EmphasisColor - static [String]$ErrorColor - static [String]$KeywordColor - static [String]$MemberColor - static [String]$NumberColor - static [String]$OperatorColor - static [String]$ParameterColor - static [String]$SelectionColor - static [String]$StringColor - static [String]$TypeColor - static [String]$VariableColor - - # Hardcoded (if valid Get-PSReadLineOption) - static [String]$Reset - static [String]$ResetColor - static [String]$InverseColor - static [String]$InverseOff - - Static ANSI() { - $PSReadLineOption = try { Get-PSReadLineOption -ErrorAction SilentlyContinue } catch { $null } - if (-not $PSReadLineOption) { return } - $ANSIType = [ANSI] -as [Type] - foreach ($Property in [ANSI].GetProperties()) { - $PSReadLineProperty = $PSReadLineOption.PSObject.Properties[$Property.Name] - if ($PSReadLineProperty) { - $ANSIType.GetProperty($Property.Name).SetValue($Property.Name, $PSReadLineProperty.Value) - } - } - $Esc = [char]0x1b - [ANSI]::Reset = "$Esc[0m" - [ANSI]::ResetColor = "$Esc[39m" - [ANSI]::InverseColor = "$Esc[7m" - [ANSI]::InverseOff = "$Esc[27m" - } -} -Class TextStyle { - hidden [String]$Text - hidden [String]$AnsiCode - hidden [String]$ResetCode = [ANSI]::Reset - TextStyle ([String]$Text, [String]$AnsiCode, [String]$ResetCode) { - $this.Text = $Text - $this.AnsiCode = $AnsiCode - $this.ResetCode = $ResetCode - } - TextStyle ([String]$Text, [String]$AnsiCode) { - $this.Text = $Text - $this.AnsiCode = $AnsiCode - } - [String] ToString() { - if ($this.ResetCode -eq [ANSI]::ResetColor) { - return "$($this.AnsiCode)$($this.Text.Replace($this.ResetCode, $this.AnsiCode))$($this.ResetCode)" - } - else { - return "$($this.AnsiCode)$($this.Text)$($this.ResetCode)" - } - } -} -class XdnName { - hidden [Bool]$_Literal - hidden $_IsVerbatim - hidden $_ContainsWildcard - hidden $_Value - - hidden Initialize($Value, $Literal) { - $this._Value = $Value - if ($Null -ne $Literal) { $this._Literal = $Literal } else { $this._Literal = $this.IsVerbatim() } - if ($this._Literal) { - $XdnName = [XdnName]::new() - $XdnName._ContainsWildcard = $False - } - else { - $XdnName = [XdnName]::new() - $XdnName._ContainsWildcard = $null - } - - } - XdnName() {} - XdnName($Value) { $this.Initialize($Value, $null) } - XdnName($Value, [Bool]$Literal) { $this.Initialize($Value, $Literal) } - static [XdnName]Literal($Value) { return [XdnName]::new($Value, $true) } - static [XdnName]Expression($Value) { return [XdnName]::new($Value, $false) } - - [Bool] IsVerbatim() { - if ($Null -eq $this._IsVerbatim) { - $this._IsVerbatim = $this._Value -is [String] -and $this._Value -Match '^[\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}_][\?\*\p{L}\p{Lt}\p{Lm}\p{Lo}\p{Nd}_]*$' # https://stackoverflow.com/questions/62754771/unquoted-key-rules-and-best-practices - } - return $this._IsVerbatim - } - - [Bool] ContainsWildcard() { - if ($Null -eq $this._ContainsWildcard) { - $this._ContainsWildcard = $this._Value -is [String] -and $this._Value -Match '(?<=([^`]|^)(``)*)[\?\*]' - } - return $this._ContainsWildcard - } - - [Bool] Equals($Object) { - if ($this._Literal) { return $this._Value -eq $Object } - elseif ($this.ContainsWildcard()) { return $Object -Like $this._Value } - else { return $this._Value -eq $Object } - } - - [String] ToString($Colored) { - $Color = if ($Colored) { - if ($this._Literal) { [ANSI]::VariableColor } - elseif (-not $this.IsVerbatim()) { [ANSI]::StringColor } - elseif ($this.ContainsWildcard()) { [ANSI]::EmphasisColor } - else { [ANSI]::VariableColor } - } - $String = - if ($this._Literal) { "'" + "$($this._Value)".Replace("'", "''") + "'" } - else { "$($this._Value)" -replace '(?$($Node.Path)" - } - - hidden [List[Ast]] GetAstSelectors ($Ast) { - $List = [List[Ast]]::new() - if ($Ast -isnot [Ast]) { - $Ast = [Parser]::ParseInput("`$_$Ast", [ref]$Null, [ref]$Null) - $Ast = $Ast.EndBlock.Statements.PipeLineElements.Expression - } - if ($Ast -is [IndexExpressionAst]) { - $List.AddRange($this.GetAstSelectors($Ast.Target)) - $List.Add($Ast) - } - elseif ($Ast -is [MemberExpressionAst]) { - $List.AddRange($this.GetAstSelectors($Ast.Expression)) - $List.Add($Ast) - } - elseif ($Ast.Extent.Text -ne '$_') { - Throw "Parse error: $($Ast.Extent.Text)" - } - return $List - } - - [List[PSNode]]GetNodeList($Levels, [Bool]$LeafNodesOnly) { - $NodeList = [List[PSNode]]::new() - $Stack = [Stack]::new() - $Stack.Push($this.get_ChildNodes().GetEnumerator()) - $Level = 1 - While ($Stack.Count -gt 0) { - $Enumerator = $Stack.Pop() - $Level-- - while ($Enumerator.MoveNext()) { - $Node = $Enumerator.Current - if ($Node.MaxDepthReached() -or ($Levels -ge 0 -and $Level -ge $Levels)) { break } - if (-not $LeafNodesOnly -or $Node -is [PSLeafNode]) { $NodeList.Add($Node) } - if ($Node -is [PSCollectionNode]) { - $Stack.Push($Enumerator) - $Level++ - $Enumerator = $Node.get_ChildNodes().GetEnumerator() - } - } - } - return $NodeList - } - [List[PSNode]]GetNodeList() { return $this.GetNodeList(1, $False) } - [List[PSNode]]GetNodeList([Int]$Levels) { return $this.GetNodeList($Levels, $False) } - hidden [PSNode[]]get_DescendantNodes() { return $this.GetNodeList(-1, $False) } - hidden [PSNode[]]get_LeafNodes() { return $this.GetNodeList(-1, $True) } - - Sort() { $this.Sort($Null, 0) } - Sort([ObjectComparison]$ObjectComparison) { $this.Sort($Null, $ObjectComparison) } - Sort([String[]]$PrimaryKey) { $this.Sort($PrimaryKey, 0) } - Sort([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.Sort($PrimaryKey, $ObjectComparison) } - Sort([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { - # As the child nodes are sorted first, we just do a side-by-side node compare: - $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' - $ObjectComparison = $ObjectComparison -band (-1 - [ObjectComparison]'IgnoreListOrder') - $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $this.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) - } - - hidden SortRecurse([PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { - $NodeList = $this.GetNodeList() - foreach ($Node in $NodeList) { - if ($Node -is [PSCollectionNode]) { $Node.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) } - } - if ($this -is [PSListNode]) { - $NodeList.Sort($PSListNodeComparer) - if ($NodeList.Count) { $this._Value = @($NodeList.Value) } else { $this._Value = @() } - } - else { # if ($Node -is [PSMapNode]) - $NodeList.Sort($PSMapNodeComparer) - $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) - foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) - if ($this -is [PSObjectNode]) { $this._Value = [PSCustomObject]$Properties } else { $this._Value = $Properties } - } - } -} -Class PSListNode : PSCollectionNode { - hidden static PSListNode() { Use-ClassAccessors } - - hidden PSListNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } - } - - hidden [Object]get_Count() { - return $this._Value.get_Count() - } - - hidden [Object]get_Names() { - if ($this._Value.Length) { return ,@(0..($this._Value.Length - 1)) } - return ,@() - } - - hidden [Object]get_Values() { - return ,@($this._Value) - } - - hidden [Object]get_CaseMatters() { return $false } - - [Bool]Contains($Index) { - return $Index -ge 0 -and $Index -lt $this.get_Count() - } - - [Bool]Exists($Index) { - return $Index -ge 0 -and $Index -lt $this.get_Count() - } - - [Object]GetValue($Index) { return $this._Value[$Index] } - [Object]GetValue($Index, $Default) { - if (-not $This.Contains($Index)) { return $Default } - return $this._Value[$Index] - } - - SetValue($Index, $Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value[$Index] = $Value - } - - Add($Value) { - if ($Value -is [PSNode]) { $Value = $Value._Value } - if ($this._Value.GetType().GetMethod('Add')) { $null = $This._Value.Add($Value) } - else { $this._Value = ($this._Value + $Value) -as $this._Value.GetType() } - $this.Cache.Remove('ChildNodes') - } - - Remove($Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - if (-not $this.Value.Contains($Value)) { return } - if ($this.Value.GetType().GetMethod('Remove')) { $null = $this._value.remove($Value) } - else { - $cList = [List[Object]]::new() - $iList = [List[Object]]::new() - $ceq = $false - foreach ($ChildNode in $this.get_ChildNodes()) { - if (-not $ceq -and $ChildNode.Value -ceq $Value) { $ceq = $true } else { $cList.Add($ChildNode.Value) } - if (-not $ceq -and $ChildNode.Value -ine $Value) { $iList.Add($ChildNode.Value) } - } - if ($ceq) { $this._Value = $cList -as $this._Value.GetType() } - else { $this._Value = $iList -as $this._Value.GetType() } - } - $this.Cache.Remove('ChildNodes') - } - - RemoveAt([Int]$Index) { - if ($Index -lt 0 -or $Index -ge $this.Value.Count) { Throw 'Index was out of range. Must be non-negative and less than the size of the collection.' } - if ($this.Value.GetType().GetMethod('RemoveAt')) { $null = $this._Value.removeAt($Index) } - else { - $this._Value = $(for ($i = 0; $i -lt $this._Value.Count; $i++) { - if ($i -ne $index) { $this._Value[$i] } - }) -as $this.ValueType - } - $this.Cache.Remove('ChildNodes') - } - - [Object]GetChildNode([Int]$Index) { - if ($this.MaxDepthReached()) { return @() } - $Count = $this._Value.get_Count() - if ($Index -lt -$Count -or $Index -ge $Count) { throw "The $($this.Path) doesn't contain a child index: $Index" } - if ($Index -lt 0) { $Index = $Count + $Index } # Negative index - if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = [Dictionary[Int,Object]]::new() } - if ( - -not $this.Cache.ChildNode.ContainsKey($Index) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Index]._Value, $this._Value[$Index]) - ) { - $Node = [PSNode]::ParseInput($this._Value[$Index]) - $Node._Name = $Index - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Index] = $Node - } - return $this.Cache.ChildNode[$Index] - } - - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = for ($Index = 0; $Index -lt $this._Value.get_Count(); $Index++) { $this.GetChildNode($Index) } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } - } - return $this.Cache['ChildNodes'] - } - - [Int]GetHashCode($CaseSensitive) { - # The hash of a list node is equal if all items match the order and the case. - # The primary keys and the list type are not relevant - if ($null -eq $this._HashCode) { - $this._HashCode = [Dictionary[bool,int]]::new() - $this._ReferenceHashCode = [Dictionary[bool,int]]::new() - } - $ReferenceHashCode = $This._value.GetHashCode() - if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { - $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode - $HashCode = '@()'.GetHashCode() # Empty lists have a common hash that is not 0 - $Index = 0 - foreach ($Node in $this.GetNodeList()) { - $HashCode = $HashCode -bxor "$Index.$($Node.GetHashCode($CaseSensitive))".GetHashCode() - $index++ - } - $this._HashCode[$CaseSensitive] = $HashCode - } - return $this._HashCode[$CaseSensitive] - } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } -} -Class PSMapNode : PSCollectionNode { - hidden static PSMapNode() { Use-ClassAccessors } - - [Int]GetHashCode($CaseSensitive) { - # The hash of a map node is equal if all names and items match the order and the case. - # The map type is not relevant - if ($null -eq $this._HashCode) { - $this._HashCode = [Dictionary[bool,int]]::new() - $this._ReferenceHashCode = [Dictionary[bool,int]]::new() - } - $ReferenceHashCode = $This._value.GetHashCode() - if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) { - $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode - $HashCode = '@{}'.GetHashCode() # Empty maps have a common hash that is not 0 - $Index = 0 - foreach ($Node in $this.GetNodeList()) { - $Name = if ($CaseSensitive) { $Node._Name } else { $Node._Name.ToUpper() } - $HashCode = $HashCode -bxor "$Index.$Name=$($Node.GetHashCode())".GetHashCode() - $Index++ - } - $this._HashCode[$CaseSensitive] = $HashCode - } - return $this._HashCode[$CaseSensitive] - } -} -Class PSDictionaryNode : PSMapNode { - hidden static PSDictionaryNode() { Use-ClassAccessors } - - hidden PSDictionaryNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } - } - - hidden [Object]get_Count() { - return $this._Value.get_Count() - } - - hidden [Object]get_Names() { - return ,$this._Value.get_Keys() - } - - hidden [Object]get_Values() { - return ,$this._Value.get_Values() - } - - hidden [Object]get_CaseMatters() { #Returns Nullable[Boolean] - if (-not $this.Cache.ContainsKey('CaseMatters')) { - $this.Cache['CaseMatters'] = $null # else $Null means that there is no key with alphabetic characters in the dictionary - foreach ($Key in $this._Value.Get_Keys()) { - if ($Key -is [String] -and $Key -match '[a-z]') { - $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } - $this.Cache['CaseMatters'] = -not $this.Contains($Case) -or $Case -cin $this._Value.Get_Keys() - break - } - } - } - return $this.Cache['CaseMatters'] - } - - [Bool]Contains($Key) { - if ($this._Value.GetType().GetMethod('ContainsKey')) { - return $this._Value.ContainsKey($Key) - } - else { - return $this._Value.Contains($Key) - } - } - [Bool]Exists($Key) { return $this.Contains($Key) } - - [Object]GetValue($Key) { return $this._Value[$Key] } - [Object]GetValue($Key, $Default) { - if (-not $This.Contains($Key)) { return $Default } - return $this._Value[$Key] - } - - SetValue($Key, $Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value[$Key] = $Value - $this.Cache.Remove('ChildNodes') - } - - Add($Key, $Value) { - if ($this.Contains($Key)) { Throw "Item '$Key' has already been added." } - if ($Value -is [PSNode]) { $Value = $Value.Value } - $this._Value.Add($Key, $Value) - $this.Cache.Remove('ChildNodes') - } - - Remove($Key) { - $null = $this._Value.Remove($Key) - $this.Cache.Remove('ChildNodes') - } - - hidden RemoveAt($Key) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) } - if (-not $this.Contains($Key)) { Throw "Item '$Key' doesn't exist." } - $null = $this._Value.Remove($Key) - $this.Cache.Remove('ChildNodes') - } - - [Object]GetChildNode([Object]$Key) { - if ($this.MaxDepthReached()) { return @() } - if (-not $this.Contains($Key)) { Throw "The $($this.Path) doesn't contain a child named: $Key" } - if (-not $this.Cache.ContainsKey('ChildNode')) { - # The ChildNode cache case sensitivity is based on the current dictionary population. - # The ChildNode cache is always ordinal, if the contained dictionary is invariant, extra entries might - # appear in the cache but shouldn't effect the results other than slightly slow down the performance. - # In other words, do not use the cache to count the entries. Custom comparers are not supported. - $this.Cache['ChildNode'] = if ($this.get_CaseMatters()) { [HashTable]::new() } else { @{} } # default is case insensitive - } - elseif ( - -not $this.Cache.ChildNode.ContainsKey($Key) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key]._Value, $this._Value[$Key]) - ) { - if($null -eq $this.get_CaseMatters()) { # If the case was undetermined, check the new key for case sensitivity - $this.Cache.CaseMatters = if ($Key -is [String] -and $Key -match '[a-z]') { - $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() } - -not $this._Value.Contains($Case) -or $Case -cin $this._Value.Get_Keys() - } - if ($this.get_CaseMatters()) { - $ChildNode = $this.Cache['ChildNode'] - $this.Cache['ChildNode'] = [HashTable]::new() # Create a new cache as it appears to be case sensitive - foreach ($Key in $ChildNode.get_Keys()) { # Migrate the content - $this.Cache.ChildNode[$Key] = $ChildNode[$Key] - } - } - } - } - if ( - -not $this.Cache.ChildNode.ContainsKey($Key) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key].Value, $this._Value[$Key]) - ) { - $Node = [PSNode]::ParseInput($this._Value[$Key]) - $Node._Name = $Key - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Key] = $Node - } - return $this.Cache.ChildNode[$Key] - } - - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = foreach ($Key in $this._Value.get_Keys()) { $this.GetChildNode($Key) } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } - } - return $this.Cache['ChildNodes'] - } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } -} -Class PSObjectNode : PSMapNode { - hidden static PSObjectNode() { Use-ClassAccessors } - - hidden PSObjectNode($Object) { - if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object } - } - - hidden [Object]get_Count() { - return @($this._Value.PSObject.Properties).get_Count() - } - - hidden [Object]get_Names() { - return ,$this._Value.PSObject.Properties.Name - } - - hidden [Object]get_Values() { - return ,$this._Value.PSObject.Properties.Value - } - - hidden [Object]get_CaseMatters() { return $false } - - [Bool]Contains($Name) { - return $this._Value.PSObject.Properties[$Name] - } - - [Bool]Exists($Name) { - return $this._Value.PSObject.Properties[$Name] - } - - [Object]GetValue($Name) { return $this._Value.PSObject.Properties[$Name].Value } - [Object]GetValue($Name, $Default) { - if (-not $this.Contains($Name)) { return $Default } - return $this._Value[$Name] - } - - SetValue($Name, $Value) { - if ($Value -is [PSNode]) { $Value = $Value.Value } - if ($this._Value -isnot [PSCustomObject]) { - $Properties = [Ordered]@{} - foreach ($Property in $this._Value.PSObject.Properties) { $Properties[$Property.Name] = $Property.Value } - $Properties[$Name] = $Value - $this._Value = [PSCustomObject]$Properties - $this.Cache.Remove('ChildNodes') - } - elseif ($this._Value.PSObject.Properties[$Name]) { - $this._Value.PSObject.Properties[$Name].Value = $Value - } - else { - Add-Member -InputObject $this._Value -Type NoteProperty -Name $Name -Value $Value - $this.Cache.Remove('ChildNodes') - } - } - - Add($Name, $Value) { - if ($this.Contains($Name)) { Throw "Item '$Name' has already been added." } - $this.SetValue($Name, $Value) - } - - Remove($Name) { - $this._Value.PSObject.Properties.Remove($Name) - $this.Cache.Remove('ChildNodes') - } - - hidden RemoveAt($Name) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) } - if (-not $this.Contains($Name)) { Throw "Item '$Name' doesn't exist." } - $this._Value.PSObject.Properties.Remove($Name) - $this.Cache.Remove('ChildNodes') - } - - [Object]GetChildNode([String]$Name) { - if ($this.MaxDepthReached()) { return @() } - if (-not $this.Contains($Name)) { Throw Throw "$($this.GetPathName('')) doesn't contain a child named: $Name" } - if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = @{} } # Object properties are case insensitive - if ( - -not $this.Cache.ChildNode.ContainsKey($Name) -or - -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Name]._Value, $this._Value.PSObject.Properties[$Name].Value) - ) { - $Node = [PSNode]::ParseInput($this._Value.PSObject.Properties[$Name].Value) - $Node._Name = $Name - $Node.Depth = $this.Depth + 1 - $Node.RootNode = [PSNode]$this.RootNode - $Node.ParentNode = $this - $this.Cache.ChildNode[$Name] = $Node - } - return $this.Cache.ChildNode[$Name] - } - - hidden [Object[]]get_ChildNodes() { - if (-not $this.Cache.ContainsKey('ChildNodes')) { - $ChildNodes = foreach ($Property in $this._Value.PSObject.Properties) { $this.GetChildNode($Property.Name) } - # if ($Property.Value -isnot [Reflection.MemberInfo]) { $this.GetChildNode($Property.Name) } - # } - if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] = @() } - } - return $this.Cache['ChildNodes'] - } - - [string]ToString() { - return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))" - } -} -class PSListNodeComparer : ObjectComparer, IComparer[Object] { # https://github.com/PowerShell/PowerShell/issues/23959 - PSListNodeComparer () {} - PSListNodeComparer ([String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey } - PSListNodeComparer ([ObjectComparison]$ObjectComparison) { $this.ObjectComparison = $ObjectComparison } - PSListNodeComparer ([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - PSListNodeComparer ([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.PrimaryKey = $PrimaryKey; $this.ObjectComparison = $ObjectComparison } - [int] Compare ([Object]$Node1, [Object]$Node2) { return $this.CompareRecurse($Node1, $Node2, 'Compare') } -} -Class TextColor : TextStyle { TextColor($Text, $AnsiColor) : base($Text, $AnsiColor, [ANSI]::ResetColor) {} } -Class CommandColor : TextColor { CommandColor($Text) : base($Text, [ANSI]::CommandColor) {} } -Class CommentColor : TextColor { CommentColor($Text) : base($Text, [ANSI]::CommentColor) {} } -Class ContinuationPromptColor : TextColor { ContinuationPromptColor($Text) : base($Text, [ANSI]::ContinuationPromptColor) {} } -Class DefaultTokenColor : TextColor { DefaultTokenColor($Text) : base($Text, [ANSI]::DefaultTokenColor) {} } -Class EmphasisColor : TextColor { EmphasisColor($Text) : base($Text, [ANSI]::EmphasisColor) {} } -Class ErrorColor : TextColor { ErrorColor($Text) : base($Text, [ANSI]::ErrorColor) {} } -Class KeywordColor : TextColor { KeywordColor($Text) : base($Text, [ANSI]::KeywordColor) {} } -Class MemberColor : TextColor { MemberColor($Text) : base($Text, [ANSI]::MemberColor) {} } -Class NumberColor : TextColor { NumberColor($Text) : base($Text, [ANSI]::NumberColor) {} } -Class OperatorColor : TextColor { OperatorColor($Text) : base($Text, [ANSI]::OperatorColor) {} } -Class ParameterColor : TextColor { ParameterColor($Text) : base($Text, [ANSI]::ParameterColor) {} } -Class SelectionColor : TextColor { SelectionColor($Text) : base($Text, [ANSI]::SelectionColor) {} } -Class StringColor : TextColor { StringColor($Text) : base($Text, [ANSI]::StringColor) {} } -Class TypeColor : TextColor { TypeColor($Text) : base($Text, [ANSI]::TypeColor) {} } -Class VariableColor : TextColor { VariableColor($Text) : base($Text, [ANSI]::VariableColor) {} } -Class InverseColor : TextStyle { InverseColor($Text) : base($Text, [ANSI]::InverseColor, [ANSI]::InverseOff) {} } - -#EndRegion Class - -#Region Function - -function Use-ClassAccessors { -<# -.SYNOPSIS - Implements class getter and setter accessors. - -.DESCRIPTION - The [Use-ClassAccessors][1] cmdlet updates script property of a class from the getter and setter methods. - Which are also known as [accessors or mutator methods][2]. - - The getter and setter methods should use the following syntax: - - ### getter syntax - - [] get_() { - return - } - - or: - - [Object] get_() { - return ,[] - } - ### setter syntax - - set_() { - - } - - > [!NOTE] - > A **setter** accessor requires a **getter** accessor to implement the related property. - - > [!NOTE] - > In most cases, you might want to hide the getter and setter methods using the [`hidden` keyword][3] - > on the getter and setter methods. - -.EXAMPLE - # Using class accessors - - The following example defines a getter and setter for a `value` property - and a _readonly_ property for the type of the type of the contained value. - - Install-Script -Name Use-ClassAccessors - - Class ExampleClass { - hidden $_Value - hidden [Object] get_Value() { - return $this._Value - } - hidden set_Value($Value) { - $this._Value = $Value - } - hidden [Type]get_Type() { - if ($Null -eq $this.Value) { return $Null } - else { return $this._Value.GetType() } - } - hidden static ExampleClass() { Use-ClassAccessors } - } - - $Example = [ExampleClass]::new() - - $Example.Value = 42 # Set value to 42 - $Example.Value # Returns 42 - $Example.Type # Returns [Int] type info - $Example.Type = 'Something' # Throws readonly error - -.PARAMETER Class - - Specifies the class from which the accessor need to be initialized. - Default: The class from which this function is invoked (by its static initializer). - -.PARAMETER Property - - Filters the property that requires to be (re)initialized. - Default: All properties in the given class - -.PARAMETER Force - - Indicates that the cmdlet reloads the specified accessors, - even if the accessors already have been defined for the concerned class. - -.LINK - [1]: https://github.com/iRon7/Use-ClassAccessors "Online Help" - [2]: https://en.wikipedia.org/wiki/Mutator_method "Mutator method" - [3]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#hidden-keyword "Hidden keyword in classes" -#> - param( - [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string[]]$Class, - - [Parameter(ValueFromPipelineByPropertyName=$true)] - [ValidateNotNullOrEmpty()] - [string]$Property, - - [switch]$Force - ) - - process { - $ClassNames = - if ($Class) { $Class } - else { - $Caller = (Get-PSCallStack)[1] - if ($Caller.FunctionName -ne '') { - $Caller.FunctionName - } - elseif ($Caller.ScriptName) { - $Ast = [System.Management.Automation.Language.Parser]::ParseFile($Caller.ScriptName, [ref]$Null, [ref]$Null) - $Ast.EndBlock.Statements.where{ $_.IsClass }.Name - } - } - foreach ($ClassName in $ClassNames) { - $TargetType = $ClassName -as [Type] - if (-not $TargetType) { Write-Warning "Class not found: $ClassName" } - $TypeData = Get-TypeData -TypeName $ClassName - $Members = if ($TypeData -and $TypeData.Members) { $TypeData.Members.get_Keys() } - $Methods = - if ($Property) { - $TargetType.GetMethod("get_$Property") - $TargetType.GetMethod("set_$Property") - } - else { - $NativeProperties = $TargetType.GetProperties() - $NativeNames = if ($NativeProperties) { $NativeProperties.Name } - $targetType.GetMethods().where{ - -not $_.IsStatic -and - ($_.Name -Like 'get_*' -or $_.Name -Like 'set_*') -and - $_.Name -NotLike '???__*' -and - $_.Name.SubString(4) -notin $NativeNames - } - } - $Accessors = [Ordered]@{} - foreach ($Method in $Methods) { - $Member = $Method.Name.SubString(4) - if (-not $Force -and $Member -in $Members) { continue } - $Parameters = $Method.GetParameters() - if ($Method.Name -Like 'get_*') { - if ($Parameters.Count -eq 0) { - if ($Method.ReturnType.IsArray) { - $Expression = @" -`$TargetType = '$ClassName' -as [Type] -`$Method = `$TargetType.GetMethod('$($Method.Name)') -`$Invoke = `$Method.Invoke(`$this, `$Null) -`$Output = `$Invoke -as '$($Method.ReturnType.FullName)' -if (@(`$Invoke).Count -gt 1) { `$Output } else { ,`$Output } -"@ - } - else { - $Expression = @" -`$TargetType = '$ClassName' -as [Type] -`$Method = `$TargetType.GetMethod('$($Method.Name)') -`$Method.Invoke(`$this, `$Null) -as '$($Method.ReturnType.FullName)' -"@ - } - if (-not $Accessors.Contains($Member)) { $Accessors[$Member] = @{} } - $Accessors[$Member].Value = [ScriptBlock]::Create($Expression) - } - else { Write-Warning "The getter '$($Method.Name)' is skipped as it is not parameter-less." } - } - elseif ($Method.Name -Like 'set_*') { - if ($Parameters.Count -eq 1) { - $Expression = @" -`$TargetType = '$ClassName' -as [Type] -`$Method = `$TargetType.GetMethod('$($Method.Name)') -`$Method.Invoke(`$this, `$Args) -"@ - if (-not $Accessors.Contains($Member)) { $Accessors[$Member] = @{} } - $Accessors[$Member].SecondValue = [ScriptBlock]::Create($Expression) - } - else { Write-Warning "The setter '$($Method.Name)' is skipped as it does not have a single parameter" } - } - } - foreach ($MemberName in $Accessors.get_Keys()) { - $TypeData = $Accessors[$MemberName] - if ($TypeData.Contains('Value')) { - $TypeData.TypeName = $ClassName - $TypeData.MemberType = 'ScriptProperty' - $TypeData.MemberName = $MemberName - $TypeData.Force = $Force - Update-TypeData @TypeData - } - else { Write-Warning "'[$ClassName].set_$MemberName()' accessor requires a '[$ClassName].get_$MemberName()' accessor." } - } - } - } -} - -#EndRegion Function - -#Region Cmdlet - -function Compare-ObjectGraph { -<# -.SYNOPSIS - Compare Object Graph - -.DESCRIPTION - Deep compares two Object Graph and lists the differences between them. - -.PARAMETER InputObject - The input object that will be compared with the reference object (see: [-Reference] parameter). - - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: - - ,$InputObject | Compare-ObjectGraph $Reference. - -.PARAMETER Reference - The reference that is used to compared with the input object (see: [-InputObject] parameter). - -.PARAMETER PrimaryKey - If supplied, dictionaries (including PSCustomObject or Component Objects) in a list are matched - based on the values of the `-PrimaryKey` supplied. - -.PARAMETER IsEqual - If set, the cmdlet will return a boolean (`$true` or `$false`). - As soon a Discrepancy is found, the cmdlet will immediately stop comparing further properties. - -.PARAMETER MatchCase - Unless the `-MatchCase` switch is provided, string values are considered case insensitive. - - > [!NOTE] - > Dictionary keys are compared based on the `$Reference`. - > if the `$Reference` is an object (PSCustomObject or component object), the key or name comparison - > is case insensitive otherwise the comparer supplied with the dictionary is used. - -.PARAMETER MatchType - Unless the `-MatchType` switch is provided, a loosely (inclusive) comparison is done where the - `$Reference` object is leading. Meaning `$Reference -eq $InputObject`: - - '1.0' -eq 1.0 # $false - 1.0 -eq '1.0' # $true (also $false if the `-MatchType` is provided) - -.PARAMETER IgnoreLisOrder - By default, items in a list are matched independent of the order (meaning by index position). - If the `-IgnoreListOrder` switch is supplied, any list in the `$InputObject` is searched for a match - with the reference. - - > [!NOTE] - > Regardless the list order, any dictionary lists are matched by the primary key (if supplied) first. - -.PARAMETER MatchMapOrder - By default, items in dictionary (including properties of an PSCustomObject or Component Object) are - matched by their key name (independent of the order). - If the `-MatchMapOrder` switch is supplied, each entry is also validated by the position. - - > [!NOTE] - > A `[HashTable]` type is unordered by design and therefore, regardless the `-MatchMapOrder` switch, - the order of the `[HashTable]` (defined by the `$Reference`) are always ignored. - -.PARAMETER MaxDepth - The maximal depth to recursively compare each embedded property (default: 10). -#> - -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Compare-ObjectGraph.md')] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] - $InputObject, - - [Parameter(Mandatory = $true, Position=0)] - $Reference, - - [String[]]$PrimaryKey, - - [Switch]$IsEqual, - - [Switch]$MatchCase, - - [Switch]$MatchType, - - [Switch]$IgnoreListOrder, - - [Switch]$MatchMapOrder, - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - $ObjectComparison = [ObjectComparison]0 - [ObjectComparison].GetEnumNames().foreach{ - if ($PSBoundParameters.ContainsKey($_) -and $PSBoundParameters[$_]) { - $ObjectComparison = $ObjectComparison -bor [ObjectComparison]$_ - } - } - - $ObjectComparer = [ObjectComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $Node1 = [PSNode]::ParseInput($Reference, $MaxDepth) -} - -process { - $Node2 = [PSNode]::ParseInput($InputObject, $MaxDepth) - if ($IsEqual) { $ObjectComparer.IsEqual($Node1, $Node2) } - else { $ObjectComparer.Report($Node1, $Node2) } -} -} -function ConvertFrom-Expression { -<# -.SYNOPSIS - Deserializes a PowerShell expression to an object. - -.DESCRIPTION - The `ConvertFrom-Expression` cmdlet safely converts a PowerShell formatted expression to an object-graph - existing of a mixture of nested arrays, hash tables and objects that contain a list of strings and values. - -.PARAMETER InputObject - Specifies the PowerShell expressions to convert to objects. Enter a variable that contains the string, - or type a command or expression that gets the string. You can also pipe a string to ConvertFrom-Expression. - - The **InputObject** parameter is required, but its value can be an empty string. - The **InputObject** value can't be `$null` or an empty string. - -.PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] - - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. - -.PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given list type. - -.PARAMETER MapAs - If supplied, the Hash table literal syntax `@{ }` syntaxes without an type initializer or with an unknown - or denied type initializer will be converted to the given map (dictionary or object) type. - -#> - -[Alias('cfe')] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ConvertFrom-Expression.md')][OutputType([Object])] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - [Alias('Expression')][String]$InputObject, - - [ValidateScript({ $_ -ne 'NoLanguage' })] - [System.Management.Automation.PSLanguageMode]$LanguageMode = 'Restricted', - - [ValidateNotNull()][Alias('ArrayAs')]$ListAs, - - [ValidateNotNull()][Alias('DictionaryAs')]$MapAs -) - -begin { - function StopError($Exception, $Id = 'IncorrectArgument', $Group = [Management.Automation.ErrorCategory]::SyntaxError, $Object){ - if ($Exception -is [System.Management.Automation.ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($Exception, $Id, $Group, $Object)) - } - - if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } - - $ListNode = if ($ListAs) { [PSNode]::ParseInput([PSInstance]::Create($ListAs)) } - $MapNode = if ($MapAs) { [PSNode]::ParseInput([PSInstance]::Create($MapAs)) } - - if ( - $ListNode -is [PSMapNode] -and $MapNode -is [PSListNode] -or - -not $ListNode -and $MapNode -is [PSListNode] -or - $ListNode -is [PSMapNode] -and -not $MapNode - ) { - $ListNode, $MapNode = $MapNode, $ListNode # In case the parameter positions are swapped - } - - $ListType = if ($ListNode) { - if ($ListType -is [PSListNode]) { $ListNode.ValueType } - else { StopError 'The -ListAs parameter requires a string, type or an object example that supports a list structure' } - } - - $MapType = if ($MapNode) { - if ($MapNode -is [PSMapNode]) { $MapNode.ValueType } - else { StopError 'The -MapAs parameter requires a string, type or an object example that supports a map structure' } - } - if ('System.Management.Automation.PSCustomObject' -eq $MapNode.ValueType) { $MapType = 'PSCustomObject' -as [type] } # https://github.com/PowerShell/PowerShell/issues/2295 - -} - -process { - [PSDeserialize]::new($InputObject, $LanguageMode, $ListType, $MapType).Object -} -} -function ConvertTo-Expression { -<# -.SYNOPSIS - Serializes an object to a PowerShell expression. - -.DESCRIPTION - The ConvertTo-Expression cmdlet converts (serializes) an object to a PowerShell expression. - The object can be stored in a variable, (.psd1) file or any other common storage for later use or to be ported - to another system. - - expressions might be restored to an object using the native [Invoke-Expression] cmdlet: - - $Object = Invoke-Expression ($Object | ConvertTo-Expression) - - > [!Warning] - > Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using `Invoke-Expression` - > to run a command that the user enters, verify that the command is safe to run before running it. - > In general, it is best to restore your objects using [ConvertFrom-Expression]. - - > [!Note] - > Some object types can not be reconstructed from a simple serialized expression. - -.INPUTS - Any. Each objects provided through the pipeline will converted to an expression. To concatenate all piped - objects in a single expression, use the unary comma operator, e.g.: `,$Object | ConvertTo-Expression` - -.OUTPUTS - String[]. `ConvertTo-Expression` returns a PowerShell [String] expression for each input object. - -.PARAMETER InputObject - Specifies the objects to convert to a PowerShell expression. Enter a variable that contains the objects, - or type a command or expression that gets the objects. You can also pipe one or more objects to - `ConvertTo-Expression.` - -.PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: - - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - - See the [PSNode Object Parser][1] for a detailed definition on node types. - -.PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. - - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. - - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. - -.PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) - - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode - -.PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. - - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). - -.PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - - By default the fidelity of an object expression will end if: - - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. - - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. - - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. - - > [!Note] - > The Object property `TypeId = []` is always excluded. - -.PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. - -.PARAMETER IndentSize - Specifies indent used for the nested properties. - -.PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is define by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`). - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" -#> - -[Alias('cto')] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ConvertTo-Expression.md')][OutputType([String])] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [ValidateScript({ $_ -ne 'NoLanguage' })] - [System.Management.Automation.PSLanguageMode]$LanguageMode = 'Restricted', - - [Alias('Expand')][Int]$ExpandDepth = [Int]::MaxValue, - - [Switch]$Explicit, - - [Switch]$FullTypeName, - - [Switch]$HighFidelity, - - [Switch]$ExpandSingleton, - - [String]$Indent = ' ', - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - function StopError($Exception, $Id = 'IncorrectArgument', $Group = [Management.Automation.ErrorCategory]::SyntaxError, $Object){ - if ($Exception -is [System.Management.Automation.ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($Exception, $Id, $Group, $Object)) - } - - if ($this.LanguageMode -eq 'NoLanguage') { Throw 'The language mode "NoLanguage" is not supported.' } - if (-not ('ConstrainedLanguage', 'FullLanguage' -eq $LanguageMode)) { - if ($Explicit) { StopError 'The Explicit switch requires Constrained - or FullLanguage mode.' } - if ($FullTypeName) { StopError 'The FullTypeName switch requires Constrained - or FullLanguage mode.' } - } - -} - -process { - $Node = [PSNode]::ParseInput($InputObject, $MaxDepth) - - [PSSerialize]::new( - $Node, - $LanguageMode, - $ExpandDepth, - $Explicit, - $FullTypeName, - $HighFidelity, - $ExpandSingleton, - $Indent - ) -} -} -function Copy-ObjectGraph { -<# -.SYNOPSIS - Copy object graph - -.DESCRIPTION - Recursively ("deep") copies a object graph. - -.EXAMPLE - # Deep copy a complete object graph into a new object graph - - $NewObjectGraph = Copy-ObjectGraph $ObjectGraph - -.EXAMPLE - # Copy (convert) an object graph using common PowerShell arrays and PSCustomObjects - - $PSObject = Copy-ObjectGraph $Object -ListAs [Array] -DictionaryAs PSCustomObject - -.EXAMPLE - # Convert a Json string to an object graph with (case insensitive) ordered dictionaries - - $PSObject = $Json | ConvertFrom-Json | Copy-ObjectGraph -DictionaryAs ([Ordered]@{}) - -.PARAMETER InputObject - The input object that will be recursively copied. - -.PARAMETER ListAs - If supplied, lists will be converted to the given type (or type of the supplied object example). - -.PARAMETER DictionaryAs - If supplied, dictionaries will be converted to the given type (or type of the supplied object example). - This parameter also accepts the [`PSCustomObject`][1] types - By default (if the [-DictionaryAs] parameters is omitted), - [`Component`][2] objects will be converted to a [`PSCustomObject`][1] type. - -.PARAMETER ExcludeLeafs - If supplied, only the structure (lists, dictionaries, [`PSCustomObject`][1] types and [`Component`][2] types will be copied. - If omitted, each leaf will be shallow copied - -.LINK - [1]: https://learn.microsoft.com/dotnet/api/system.management.automation.pscustomobject "PSCustomObject Class" - [2]: https://learn.microsoft.com/dotnet/api/system.componentmodel.component "Component Class" -#> -[Alias('Copy-Object', 'cpo')] -[OutputType([Object[]])] -[CmdletBinding(DefaultParameterSetName = 'ListAs', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Copy-ObjectGraph.md')] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] - $InputObject, - - [ValidateNotNull()][Alias('ArrayAs')]$ListAs, - - [ValidateNotNull()][Alias('DictionaryAs')]$MapAs, - - [Switch]$ExcludeLeafs, - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) -begin { - function StopError($Exception, $Id = 'IncorrectArgument', $Group = [Management.Automation.ErrorCategory]::SyntaxError, $Object){ - if ($Exception -is [System.Management.Automation.ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($Exception, $Id, $Group, $Object)) - } - - $ListNode = if ($PSBoundParameters.ContainsKey('ListAs')) { [PSNode]::ParseInput([PSInstance]::Create($ListAs)) } - $MapNode = if ($PSBoundParameters.ContainsKey('MapAs')) { [PSNode]::ParseInput([PSInstance]::Create($MapAs)) } - - if ( - $ListNode -is [PSMapNode] -and $MapNode -is [PSListNode] -or - -not $ListNode -and $MapNode -is [PSListNode] -or - $ListNode -is [PSMapNode] -and -not $MapNode - ) { - $ListNode, $MapNode = $MapNode, $ListNode # In case the parameter positions are swapped - } - - $ListType = if ($ListNode) { - if ($ListNode -is [PSListNode]) { $ListNode.ValueType } - else { StopError 'The -ListAs parameter requires a string, type or an object example that supports a list structure' } - } - - $MapType = if ($MapNode) { - if ($MapNode -is [PSMapNode]) { $MapNode.ValueType } - else { StopError 'The -MapAs parameter requires a string, type or an object example that supports a map structure' } - } - if ('System.Management.Automation.PSCustomObject' -eq $MapNode.ValueType) { $MapType = 'PSCustomObject' -as [type] } # https://github.com/PowerShell/PowerShell/issues/2295 - - function CopyObject( - [PSNode]$Node, - [Type]$ListType, - [Type]$MapType, - [Switch]$ExcludeLeafs - ) { - if ($Node -is [PSLeafNode]) { - if ($ExcludeLeafs -or $Null -eq $Node.Value) { return $Node.Value } - else { $Node.Value.PSObject.Copy() } - } - elseif ($Node -is [PSListNode]) { - $Type = if ($Null -ne $ListType) { $ListType } else { $Node.ValueType } - $Values = foreach ($ChildNode in $Node.ChildNodes) { CopyObject $ChildNode -ListType $ListType -MapType $MapType } - $Values = $Values -as $Type - ,$Values - } - elseif ($Node -is [PSMapNode]) { - $Type = if ($Null -ne $MapType) { $MapType } else { $Node.ValueType } - $IsDirectory = $Null -ne $Type.GetInterface('IDictionary') - if ($Type.FullName -eq 'System.Collections.Hashtable') { $Dictionary = @{} } # Case insensitive - elseif ($IsDirectory) { $Dictionary = New-Object -Type $Type } - else { $Dictionary = [Ordered]@{} } - foreach ($ChildNode in $Node.ChildNodes) { $Dictionary[[Object]$ChildNode.Name] = CopyObject $ChildNode -ListType $ListType -MapType $MapType } - if ($IsDirectory) { $Dictionary } else { [PSCustomObject]$Dictionary } - } - } -} -process { - $PSNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - CopyObject $PSNode -ListType $ListType -MapType $MapType -ExcludeLeafs:$ExcludeLeafs -} -} -function Export-ObjectGraph { -<# -.SYNOPSIS - Serializes a PowerShell File or object-graph and exports it to a PowerShell (data) file. - -.DESCRIPTION - The `Export-ObjectGraph` cmdlet converts a PowerShell (complex) object to an PowerShell expression - and exports it to a PowerShell (`.ps1`) file or a PowerShell data (`.psd1`) file. - -.PARAMETER Path - Specifies the path to a file where `Export-ObjectGraph` exports the ObjectGraph. - Wildcard characters are permitted. - -.PARAMETER LiteralPath - Specifies a path to one or more locations where PowerShell should export the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. - -.PARAMETER LanguageMode - Defines which object types are allowed for the serialization, see: [About language modes][2] - If a specific type isn't allowed in the given language mode, it will be substituted by: - - * **`$Null`** in case of a null value - * **`$False`** in case of a boolean false - * **`$True`** in case of a boolean true - * **A number** in case of a primitive value - * **A string** in case of a string or any other **leaf** node - * `@(...)` for an array (**list** node) - * `@{...}` for any dictionary, PSCustomObject or Component (aka **map** node) - - See the [PSNode Object Parser][1] for a detailed definition on node types. - -.PARAMETER ExpandDepth - Defines up till what level the collections will be expanded in the output. - - * A `-ExpandDepth 0` will create a single line expression. - * A `-ExpandDepth -1` will compress the single line by removing command spaces. - - > [!Note] - > White spaces (as newline characters and spaces) will not be removed from the content - > of a (here) string. - -.PARAMETER Explicit - By default, restricted language types initializers are suppressed. - When the `Explicit` switch is set, *all* values will be prefixed with an initializer - (as e.g. `[Long]` and `[Array]`) - - > [!Note] - > The `-Explicit` switch can not be used in **restricted** language mode - -.PARAMETER FullTypeName - In case a value is prefixed with an initializer, the full type name of the initializer is used. - - > [!Note] - > The `-FullTypename` switch can not be used in **restricted** language mode and will only be - > meaningful if the initializer is used (see also the [-Explicit] switch). - -.PARAMETER HighFidelity - If the `-HighFidelity` switch is supplied, all nested object properties will be serialized. - - By default the fidelity of an object expression will end if: - - 1) the (embedded) object is a leaf node (see: [PSNode Object Parser][1]) - 2) the (embedded) object expression is able to round trip. - - An object is able to roundtrip if the resulted expression of the object itself or one of - its properties (prefixed with the type initializer) can be used to rebuild the object. - - The advantage of the default fidelity is that the resulted expression round trips (aka the - object might be rebuild from the expression), the disadvantage is that information hold by - less significant properties is lost (as e.g. timezone information in a `DateTime]` object). - - The advantage of the high fidelity switch is that all the information of the underlying - properties is shown, yet any constrained or full object type will likely fail to rebuild - due to constructor limitations such as readonly property. - - > [!Note] - > Objects properties of type `[Reflection.MemberInfo]` are always excluded. - -.PARAMETER ExpandSingleton - (List or map) collections nodes that contain a single item will not be expanded unless this - `-ExpandSingleton` is supplied. - -.PARAMETER IndentSize - Specifies indent used for the nested properties. - -.PARAMETER MaxDepth - Specifies how many levels of contained objects are included in the PowerShell representation. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). - -.PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" -#> - -[Alias('Export-Object', 'epo')] -[CmdletBinding(DefaultParameterSetName='Path', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Export-ObjectGraph.md')] -param( - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] - [string[]] - $Path, - - [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] - [Alias('PSPath','LP')] - [string[]] - $LiteralPath, - - [ValidateScript({ $_ -ne 'NoLanguage' })] - [System.Management.Automation.PSLanguageMode]$LanguageMode, - - [Alias('Expand')][Int]$ExpandDepth = [Int]::MaxValue, - - [Switch]$Explicit, - - [Switch]$FullTypeName, - - [Switch]$HighFidelity, - - [Switch]$ExpandSingleton, - - [String]$Indent = ' ', - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth, - - [ValidateNotNullOrEmpty()]$Encoding -) - -begin { - $Extension = if ($Path) { [System.IO.Path]::GetExtension($Path) } else { [System.IO.Path]::GetExtension($LiteralPath) } - if (-not $PSBoundParameters.ContainsKey('LanguageMode')) { - $PSBoundParameters['LanguageMode'] = if ($Extension -eq '.psd1') { 'Restricted' } else { 'Constrained' } - } - - $ToExpressionParameters = 'LanguageMode', 'ExpandDepth', 'Explicit', 'FullTypeName', '$HighFidelity', 'ExpandSingleton', 'Indent', 'MaxDepth' - $ToExpressionArguments = @{} - $ToExpressionParameters.where{ $PSBoundParameters.ContainsKey($_) }.foreach{ $ToExpressionArguments[$_] = $PSBoundParameters[$_] } - $ToExpressionContext = $ExecutionContext.InvokeCommand.GetCommand('ObjectGraphTools\ConvertTo-Expression', [System.Management.Automation.CommandTypes]::Cmdlet) - $ToExpressionPipeline = { & $ToExpressionContext @ToExpressionArguments }.GetSteppablePipeline() - $ToExpressionPipeline.Begin($True) - - $SetContentArguments = @{} - @('Path', 'LiteralPath', 'Encoding').where{ $PSBoundParameters.ContainsKey($_) }.foreach{ $SetContentArguments[$_] = $PSBoundParameters[$_] } -} - -process { - $Expression = $ToExpressionPipeline.Process($InputObject) - Set-Content @SetContentArguments -Value $Expression -} - -end { - $ToExpressionPipeline.End() -} -} -function Get-ChildNode { -<# -.SYNOPSIS - Gets the child nodes of an object-graph - -.DESCRIPTION - Gets the (unique) nodes and child nodes in one or more specified locations of an object-graph - The returned nodes are unique even if the provide list of input parent nodes have an overlap. - -.EXAMPLE - # Select all leaf nodes in a object graph - - Given the following object graph: - - $Object = @{ - Comment = 'Sample ObjectGraph' - Data = @( - @{ - Index = 1 - Name = 'One' - Comment = 'First item' - } - @{ - Index = 2 - Name = 'Two' - Comment = 'Second item' - } - @{ - Index = 3 - Name = 'Three' - Comment = 'Third item' - } - ) - } - - The following example will receive all leaf nodes: - - $Object | Get-ChildNode -Recurse -Leaf - - Path Name Depth Value - ---- ---- ----- ----- - .Data[0].Comment Comment 3 First item - .Data[0].Name Name 3 One - .Data[0].Index Index 3 1 - .Data[1].Comment Comment 3 Second item - .Data[1].Name Name 3 Two - .Data[1].Index Index 3 2 - .Data[2].Comment Comment 3 Third item - .Data[2].Name Name 3 Three - .Data[2].Index Index 3 3 - .Comment Comment 1 Sample ObjectGraph - -.EXAMPLE - # update a property - - The following example selects all child nodes named `Comment` at a depth of `3`. - Than filters the one that has an `Index` sibling with the value `2` and eventually - sets the value (of the `Comment` node) to: 'Two to the Loo'. - - $Object | Get-ChildNode -AtDepth 3 -Include Comment | - Where-Object { $_.ParentNode.GetChildNode('Index').Value -eq 2 } | - ForEach-Object { $_.Value = 'Two to the Loo' } - - ConvertTo-Expression $Object - - @{ - Data = - @{ - Comment = 'First item' - Name = 'One' - Index = 1 - }, - @{ - Comment = 'Two to the Loo' - Name = 'Two' - Index = 2 - }, - @{ - Comment = 'Third item' - Name = 'Three' - Index = 3 - } - Comment = 'Sample ObjectGraph' - } - - See the [PowerShell Object Parser][1] For details on the `[PSNode]` properties and methods. - -.PARAMETER InputObject - The concerned object graph or node. - -.PARAMETER Recurse - Recursively iterates through all embedded property objects (nodes) to get the selected nodes. - The maximum depth of of a specific node that might be retrieved is define by the `MaxDepth` - of the (root) node. To change the maximum depth the (root) node needs to be loaded first, e.g.: - - Get-Node -Depth 20 | Get-ChildNode ... - - (See also: [`Get-Node`][2]) - - > [!NOTE] - > If the [AtDepth] parameter is supplied, the object graph is recursively searched anyways - > for the selected nodes up till the deepest given `AtDepth` value. - -.PARAMETER AtDepth - When defined, only returns nodes at the given depth(s). - - > [!NOTE] - > The nodes below the `MaxDepth` can not be retrieved. - -.PARAMETER ListChild - Returns the closest nodes derived from a **list node**. - -.PARAMETER Include - Returns only nodes derived from a **map node** including only the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. - - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. - -.PARAMETER Exclude - Returns only nodes derived from a **map node** excluding the ones specified by one or more - string patterns defined by this parameter. Wildcard characters are permitted. - - > [!NOTE] - > The [-Include] and [-Exclude] parameters can be used together. However, the exclusions are applied - > after the inclusions, which can affect the final output. - -.PARAMETER Literal - The values of the [-Include] - and [-Exclude] parameters are used exactly as it is typed. - No characters are interpreted as wildcards. - -.PARAMETER Leaf - Only return leaf nodes. Leaf nodes are nodes at the end of a branch and do not have any child nodes. - You can use the [-Recurse] parameter with the [-Leaf] parameter. - -.PARAMETER IncludeSelf - Includes the current node with the returned child nodes. - -.PARAMETER ValueOnly - returns the value of the node instead of the node itself. - -.PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: - - $Test = @{Guid = New-Guid} - $Test.Parent = $Test - - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md "Get-Node" -#> - -[Alias('gcn')] -[OutputType([PSNode[]])] -[CmdletBinding(DefaultParameterSetName='ListChild', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-ChildNode.md')] param( - [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] - $InputObject, - - [switch] - $Recurse, - - [ValidateRange(0, [int]::MaxValue)] - [int[]] - $AtDepth, - - [Parameter(ParameterSetName='ListChild')] - [switch] - $ListChild, - - [Parameter(ParameterSetName='MapChild', Position = 0)] - [string[]] - $Include, - - [Parameter(ParameterSetName='MapChild')] - [string[]] - $Exclude, - - [Parameter(ParameterSetName='MapChild')] - [switch] - $Literal, - - [switch] - $Leaf, - - [Alias('Self')][switch] - $IncludeSelf, - - [switch] - $ValueOnly, - - [Int] - $MaxDepth -) - -begin { - $SearchDepth = if ($PSBoundParameters.ContainsKey('AtDepth')) { - [System.Linq.Enumerable]::Max($AtDepth) - $Node.Depth - 1 - } elseif ($Recurse) { -1 } else { 1 } -} - -process { - if ($InputObject -is [PSNode]) { $Self = $InputObject } - else { $Self = [PSNode]::ParseInput($InputObject, $MaxDepth) } - if ($Self -is [PSCollectionNode]) { $NodeList = $Self.GetNodeList($SearchDepth, $Leaf) } - else { - Write-Warning "The node '$($Self.Path)' is a leaf node which does not contain any child nodes." - $NodeList = [System.Collections.Generic.List[Object]]::new() - } - if ($IncludeSelf) { $NodeList.Insert(0, $Self) } - foreach ($Node in $NodeList) { - if ( - ( - (-not $ListChild -and $PSCmdlet.ParameterSetName -ne 'MapChild') -or - ($ListChild -and $Node.ParentNode -is [PSListNode]) -or - ($PSCmdlet.ParameterSetName -eq 'MapChild' -and $Node.ParentNode -is [PSMapNode]) - ) -and - ( - -not $PSBoundParameters.ContainsKey('AtDepth') -or $Node.Depth -in $AtDepth - ) -and - ( - -not $Include -or ( - ($Literal -and $Node.Name -in $Include) -or - (-not $Literal -and $Include.where({ $Node.Name -like $_ }, 'first')) - ) - ) -and -not ( - $Exclude -and ( - ($Literal -and $Node.Name -in $Exclude) -or - (-not $Literal -and $Exclude.where({ $Node.Name -like $_ }, 'first')) - ) - ) - ) { - if ($ValueOnly) { $Node.Value } else { $Node } - } - } -} -} -function Get-Node { -<# -.SYNOPSIS - Get a node - -.DESCRIPTION - The Get-Node cmdlet gets the node at the specified property location of the supplied object graph. - -.EXAMPLE - # Parse a object graph to a node instance - - The following example parses a hash table to `[PSNode]` instance: - - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node - - PathName Name Depth Value - -------- ---- ----- ----- - 0 {My, Object} - -.EXAMPLE - # select a sub node in an object graph - - The following example parses a hash table to `[PSNode]` instance and selects the second (`0` indexed) - item in the `My` map node - - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node My[1] - - PathName Name Depth Value - -------- ---- ----- ----- - My[1] 1 2 2 - -.EXAMPLE - # Change the price of the **PowerShell** book: - - $ObjectGraph = - @{ - BookStore = @( - @{ - Book = @{ - Title = 'Harry Potter' - Price = 29.99 - } - }, - @{ - Book = @{ - Title = 'Learning PowerShell' - Price = 39.95 - } - } - ) - } - - ($ObjectGraph | Get-Node BookStore~Title=*PowerShell*..Price).Value = 24.95 - $ObjectGraph | ConvertTo-Expression - @{ - BookStore = @( - @{ - Book = @{ - Price = 29.99 - Title = 'Harry Potter' - } - }, - @{ - Book = @{ - Price = 24.95 - Title = 'Learning PowerShell' - } - } - ) - } - - for more details, see: [PowerShell Object Parser][1] and [Extended dot notation][2] - -.PARAMETER InputObject - The concerned object graph or node. - -.PARAMETER Path - Specifies the path to a specific node in the object graph. - The path might be either: - - * A dot-notation (`[String]`) literal or expression (as natively used with PowerShell) - * A array of strings (dictionary keys or Property names) and/or integers (list indices) - * A `[PSNodePath]` (such as `$Node.Path`) or a `[XdnPath]` (Extended Dot-Notation) object - -.PARAMETER Literal - If Literal switch is set, all (map) nodes in the given path are considered literal. - -.PARAMETER ValueOnly - returns the value of the node instead of the node itself. - -.PARAMETER Unique - Specifies that if a subset of the nodes has identical properties and values, - only a single node of the subset should be selected. - -.PARAMETER MaxDepth - Specifies the maximum depth that an object graph might be recursively iterated before it throws an error. - The failsafe will prevent infinitive loops for circular references as e.g. in: - - $Test = @{Guid = New-Guid} - $Test.Parent = $Test - - The default `MaxDepth` is defined by `[PSNode]::DefaultMaxDepth = 10`. - - > [!Note] - > The `MaxDepth` is bound to the root node of the object graph. Meaning that a descendant node - > at depth of 3 can only recursively iterated (`10 - 3 =`) `7` times. - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended dot notation" -#> - -[Alias('gn')] -[OutputType([PSNode])] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Get-Node.md')] param( - [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] - $InputObject, - - [Parameter(ParameterSetName='Path', Position=0, ValueFromPipelineByPropertyName = $true)] - $Path, - - [Parameter(ParameterSetName='Path')] - [Switch] - $Literal, - - [switch] - $ValueOnly, - - [switch] - $Unique, - - [Int] - $MaxDepth -) - -begin { - if ($Unique) { - # As we want to support case sensitive and insensitive nodes the unique nodes are matched by case - # also knowing that in most cases nodes are compared with its self. - $UniqueNodes = [System.Collections.Generic.Dictionary[String, System.Collections.Generic.HashSet[Object]]]::new() - } - $XdnPaths = @($Path).ForEach{ - if ($_ -is [XdnPath]) { $_ } - elseif ($literal) { [XdnPath]::new($_, $True) } - else { [XdnPath]$_ } - } -} - -process { - $Root = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Node = - if ($XdnPaths) { $XdnPaths.ForEach{ $Root.GetNode($_) } } - else { $Root } - if (-not $Unique -or $( - $PathName = $Node.Path.ToString() - if (-not $UniqueNodes.ContainsKey($PathName)) { - $UniqueNodes[$PathName] = [System.Collections.Generic.HashSet[Object]]::new() - } - $UniqueNodes[$PathName].Add($Node.Value) - )) { - if ($ValueOnly) { $Node.Value } else { $Node } - } -} -} -function Get-SortObjectGraph { -<# -.SYNOPSIS - Sort an object graph - -.DESCRIPTION - Recursively sorts a object graph. - -.PARAMETER InputObject - The input object that will be recursively sorted. - - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: - - ,$InputObject | Sort-Object. - -.PARAMETER PrimaryKey - Any primary key defined by the [-PrimaryKey] parameter will be put on top of [-InputObject] - independent of the (descending) sort order. - - It is allowed to supply multiple primary keys. - -.PARAMETER MatchCase - (Alias `-CaseSensitive`) Indicates that the sort is case-sensitive. By default, sorts aren't case-sensitive. - -.PARAMETER Descending - Indicates that Sort-Object sorts the objects in descending order. The default is ascending order. - - > [!NOTE] - > Primary keys (see: [-PrimaryKey]) will always put on top. - -.PARAMETER MaxDepth - The maximal depth to recursively compare each embedded property (default: 10). -#> - -[Alias('Sort-ObjectGraph', 'sro')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseApprovedVerbs', '')] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Sort-ObjectGraph.md')][OutputType([Object[]])] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Alias('By')][String[]]$PrimaryKey, - - [Alias('CaseSensitive')] - [Switch]$MatchCase, - - [Switch]$Descending, - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) -begin { - $ObjectComparison = [ObjectComparison]0 - if ($MatchCase) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchCase'} - if ($Descending) { $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'Descending'} - # As the child nodes are sorted first, we just do a side-by-side node compare: - $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder' - - $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison } - - function SortRecurse([PSCollectionNode]$Node, [PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) { - $NodeList = $Node.GetNodeList() - for ($i = 0; $i -lt $NodeList.Count; $i++) { - if ($NodeList[$i] -is [PSCollectionNode]) { - $NodeList[$i] = SortRecurse $NodeList[$i] -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer - } - } - if ($Node -is [PSListNode]) { - $NodeList.Sort($PSListNodeComparer) - if ($NodeList.Count) { $Node.Value = @($NodeList.Value) } else { $Node.Value = @() } - } - else { # if ($Node -is [PSMapNode]) - $NodeList.Sort($PSMapNodeComparer) - $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal) - foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException) - if ($Node -is [PSObjectNode]) { $Node.Value = [PSCustomObject]$Properties } else { $Node.Value = $Properties } - } - $Node - } -} - -process { - $Node = [PSNode]::ParseInput($InputObject, $MaxDepth) - if ($Node -is [PSCollectionNode]) { - $Node = SortRecurse $Node -PSListNodeComparer $PSListNodeComparer -PSMapNodeComparer $PSMapNodeComparer - } - $Node.Value -} -} -function Import-ObjectGraph { -<# -.SYNOPSIS - Deserializes a PowerShell File or any object-graphs from PowerShell file to an object. - -.DESCRIPTION - The `Import-ObjectGraph` cmdlet safely converts a PowerShell formatted expression contained by a file - to an object-graph existing of a mixture of nested arrays, hash tables and objects that contain a list - of strings and values. - -.PARAMETER Path - Specifies the path to a file where `Import-ObjectGraph` imports the object-graph. - Wildcard characters are permitted. - -.PARAMETER LiteralPath - Specifies a path to one or more locations that contain a PowerShell the object-graph. - The value of LiteralPath is used exactly as it's typed. No characters are interpreted as wildcards. - If the path includes escape characters, enclose it in single quotation marks. Single quotation marks tell - PowerShell not to interpret any characters as escape sequences. - -.PARAMETER LanguageMode - Defines which object types are allowed for the deserialization, see: [About language modes][2] - - * Any type that is not allowed by the given language mode, will be omitted leaving a bare `[ValueType]`, - `[String]`, `[Array]` or `[HashTable]`. - * Any variable that is not `$True`, `$False` or `$Null` will be converted to a literal string, e.g. `$Test`. - - The default `LanguageMode` is `Restricted` for PowerShell Data (`psd1`) files and `Constrained` for any - other files, which usually concerns PowerShell (`.ps1`) files. - - > [!Caution] - > - > In full language mode, `ConvertTo-Expression` permits all type initializers. Cmdlets, functions, - > CIM commands, and workflows will *not* be invoked by the `ConvertFrom-Expression` cmdlet. - > - > Take reasonable precautions when using the `Invoke-Expression -LanguageMode Full` command in scripts. - > Verify that the class types in the expression are safe before instantiating them. In general, it is - > best to design your configuration expressions with restricted or constrained classes, rather than - > allowing full freeform expressions. - -.PARAMETER ListAs - If supplied, the array subexpression `@( )` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given list type. - -.PARAMETER MapAs - If supplied, the array subexpression `@{ }` syntaxes without an type initializer or with an unknown or - denied type initializer will be converted to the given map (dictionary or object) type. - - The default `MapAs` is an (ordered) `PSCustomObject` for PowerShell Data (`psd1`) files and - a (unordered) `HashTable` for any other files, which usually concerns PowerShell (`.ps1`) files that - support explicit type initiators. - -.PARAMETER Encoding - Specifies the type of encoding for the target file. The default value is `utf8NoBOM`. - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser" - [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes "About language modes" -#> - -[Alias('Import-Object', 'imo')] -[CmdletBinding(DefaultParameterSetName='Path', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Import-ObjectGraph.md')] -param( - [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] - [string[]] - $Path, - - [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] - [Alias('PSPath','LP')] - [string[]] - $LiteralPath, - - [ValidateNotNull()][Alias('ArrayAs')]$ListAs, - - [ValidateNotNull()][Alias('DictionaryAs')]$MapAs, - - [ValidateScript({ $_ -ne 'NoLanguage' })] - [System.Management.Automation.PSLanguageMode]$LanguageMode, - - [ValidateNotNullOrEmpty()]$Encoding -) - -begin { - $Extension = if ($Path) { [System.IO.Path]::GetExtension($Path) } else { [System.IO.Path]::GetExtension($LiteralPath) } - if (-not $PSBoundParameters.ContainsKey('LanguageMode')) { - $PSBoundParameters['LanguageMode'] = if ($Extension -eq '.psd1') { 'Restricted' } else { 'Constrained' } - } - if (-not $PSBoundParameters.ContainsKey('MapAs') -and $Extension -eq '.psd1') { - $PSBoundParameters['MapAs'] = 'PSCustomObject' - } - - $FromExpressionParameters = 'ListAs', 'MapAs', 'LanguageMode' - $FromExpressionArguments = @{} - $FromExpressionParameters.where{ $PSBoundParameters.ContainsKey($_) }.foreach{ $FromExpressionArguments[$_] = $PSBoundParameters[$_] } - $FromExpressionContext = $ExecutionContext.InvokeCommand.GetCommand('ObjectGraphTools\ConvertFrom-Expression', [System.Management.Automation.CommandTypes]::Cmdlet) - $FromExpressionPipeline = { & $FromExpressionContext @FromExpressionArguments }.GetSteppablePipeline() - $FromExpressionPipeline.Begin($True) - - $GetContentArguments = @{} - @('Path', 'LiteralPath', 'Encoding').where{ $PSBoundParameters.ContainsKey($_) }.foreach{ $GetContentArguments[$_] = $PSBoundParameters[$_] } -} - -process { - $Expression = Get-Content @GetContentArguments -Raw - $FromExpressionPipeline.Process($Expression) -} - -end { - $FromExpressionPipeline.End() -} -} -function Merge-ObjectGraph { -<# -.SYNOPSIS - Merges two object graphs into one - -.DESCRIPTION - Recursively merges two object graphs into a new object graph. - -.PARAMETER InputObject - The input object that will be merged with the template object (see: [-Template] parameter). - - > [!NOTE] - > Multiple input object might be provided via the pipeline. - > The common PowerShell behavior is to unroll any array (aka list) provided by the pipeline. - > To avoid a list of (root) objects to unroll, use the **comma operator**: - - ,$InputObject | Compare-ObjectGraph $Template. - -.PARAMETER Template - The template that is used to merge with the input object (see: [-InputObject] parameter). - -.PARAMETER PrimaryKey - In case of a list of dictionaries or PowerShell objects, the PowerShell key is used to - link the items or properties: if the PrimaryKey exists on both the [-Template] and the - [-InputObject] and the values are equal, the dictionary or PowerShell object will be merged. - Otherwise (if the key can't be found or the values differ), the complete dictionary or - PowerShell object will be added to the list. - - It is allowed to supply multiple primary keys where each primary key will be used to - check the relation between the [-Template] and the [-InputObject]. - -.PARAMETER MaxDepth - The maximal depth to recursively compare each embedded node. - The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). -#> - -[Alias('Merge-Object', 'mgo')] -[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Scope = "Function", Justification = 'False positive')] -[CmdletBinding(HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Merge-ObjectGraph.md')][OutputType([Object[]])] param( - - [Parameter(Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(Mandatory = $true, Position=0)] - $Template, - - [String[]]$PrimaryKey, - - [Switch]$MatchCase, - - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) -begin { - function MergeObject ([PSNode]$TemplateNode, [PSNode]$ObjectNode, [String[]]$PrimaryKey, [Switch]$MatchCase) { - if ($ObjectNode -is [PSListNode] -and $TemplateNode -is [PSListNode]) { - $FoundIndices = [System.Collections.Generic.HashSet[int]]::new() - $Type = if ($ObjectNode.Value.IsFixedSize) { [Collections.Generic.List[PSObject]] } else { $ObjectNode.Value.GetType() } - $Output = New-Object -TypeName $Type - $ObjectItems = $ObjectNode.ChildNodes - $TemplateItems = $TemplateNode.ChildNodes - foreach($ObjectItem in $ObjectItems) { - $FoundNode = $False - foreach ($TemplateItem in $TemplateItems) { - if ($ObjectItem -is [PSLeafNode] -and $TemplateItem -is [PSLeafNode]) { - $Equal = if ($MatchCase) { $TemplateItem.Value -ceq $ObjectItem.Value } - else { $TemplateItem.Value -eq $ObjectItem.Value } - if ($Equal) { - $Output.Add($ObjectItem.Value) - $FoundNode = $True - $Null = $FoundIndices.Add($TemplateItem.Name) - } - } - elseif ($ObjectItem -is [PSMapNode] -and $TemplateItem -is [PSMapNode]) { - foreach ($Key in $PrimaryKey) { - if (-not $TemplateItem.Contains($Key) -or -not $ObjectItem.Contains($Key)) { continue } - if ($TemplateItem.GetChildNode($Key).Value -eq $ObjectItem.GetChildNode($Key).Value) { - $Item = MergeObject -Template $TemplateItem -Object $ObjectItem -PrimaryKey $PrimaryKey -MatchCase $MatchCase - $Output.Add($Item) - $FoundNode = $True - $Null = $FoundIndices.Add($TemplateItem.Name) - } - } - } - } - if (-not $FoundNode) { $Output.Add($ObjectItem.Value) } - } - foreach ($TemplateItem in $TemplateItems) { - if (-not $FoundIndices.Contains($TemplateItem.Name)) { $Output.Add($TemplateItem.Value) } - } - if ($ObjectNode.Value.IsFixedSize) { $Output = @($Output) } - ,$Output - } - elseif ($ObjectNode -is [PSMapNode] -and $TemplateNode -is [PSMapNode]) { - if ($ObjectNode -is [PSDictionaryNode]) { $Dictionary = New-Object -TypeName $ObjectNode.ValueType } # The $InputObject defines the map type - else { $Dictionary = [System.Collections.Specialized.OrderedDictionary]::new() } - foreach ($ObjectItem in $ObjectNode.ChildNodes) { - if ($TemplateNode.Contains($ObjectItem.Name)) { # The $InputObject defines the comparer - $Value = MergeObject -Template $TemplateNode.GetChildNode($ObjectItem.Name) -Object $ObjectItem -PrimaryKey $PrimaryKey -MatchCase $MatchCase - } - else { $Value = $ObjectItem.Value } - $Dictionary.Add($ObjectItem.Name, $Value) - } - foreach ($Key in $TemplateNode.Names) { - if (-not $Dictionary.Contains($Key)) { $Dictionary.Add($Key, $TemplateNode.GetChildNode($Key).Value) } - } - if ($ObjectNode -is [PSDictionaryNode]) { $Dictionary } else { [PSCustomObject]$Dictionary } - } - else { return $ObjectNode.Value } - } - $TemplateNode = [PSNode]::ParseInput($Template, $MaxDepth) -} -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - MergeObject $TemplateNode $ObjectNode -PrimaryKey $PrimaryKey -MatchCase $MatchCase -} -} -function Test-ObjectGraph { -<# -.SYNOPSIS -Tests the properties of an object-graph. - -.DESCRIPTION -Tests an object-graph against a schema object by verifying that the properties of the object-graph -meet the constrains defined in the schema object. - -The schema object has the following major features: - -* Independent of the object notation (as e.g. [Json (JavaScript Object Notation)][2] or [PowerShell Data Files][3]) -* Each test node is at the same level as the input node being validated -* Complex node requirements (as mutual exclusive nodes) might be selected using a logical formula - -.EXAMPLE -#Test whether a `$Person` object meats the schema requirements. - - $Person = [PSCustomObject]@{ - FirstName = 'John' - LastName = 'Smith' - IsAlive = $True - Birthday = [DateTime]'Monday, October 7, 1963 10:47:00 PM' - Age = 27 - Address = [PSCustomObject]@{ - Street = '21 2nd Street' - City = 'New York' - State = 'NY' - PostalCode = '10021-3100' - } - Phone = @{ - Home = '212 555-1234' - Mobile = '212 555-2345' - Work = '212 555-3456', '212 555-3456', '646 555-4567' - } - Children = @('Dennis', 'Stefan') - Spouse = $Null - } - - $Schema = @{ - FirstName = @{ '@Type' = 'String' } - LastName = @{ '@Type' = 'String' } - IsAlive = @{ '@Type' = 'Bool' } - Birthday = @{ '@Type' = 'DateTime' } - Age = @{ - '@Type' = 'Int' - '@Minimum' = 0 - '@Maximum' = 99 - } - Address = @{ - '@Type' = 'PSMapNode' - Street = @{ '@Type' = 'String' } - City = @{ '@Type' = 'String' } - State = @{ '@Type' = 'String' } - PostalCode = @{ '@Type' = 'String' } - } - Phone = @{ - '@Type' = 'PSMapNode', $Null - Home = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } - Mobile = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } - Work = @{ '@Match' = '^\d{3} \d{3}-\d{4}$' } - } - Children = @(@{ '@Type' = 'String', $Null }) - Spouse = @{ '@Type' = 'String', $Null } - } - - $Person | Test-Object $Schema | Should -BeNullOrEmpty - -.PARAMETER InputObject -Specifies the object to test for validity against the schema object. -The object might be any object containing embedded (or even recursive) lists, dictionaries, objects or scalar -values received from a application or an object notation as Json or YAML using their related `ConvertFrom-*` -cmdlets. - -.PARAMETER SchemaObject -Specifies a schema to validate the JSON input against. By default, if any discrepancies, toy will be reported -in a object list containing the path to failed node, the value whether the node is valid or not and the issue. -If no issues are found, the output is empty. - -For details on the schema object, see the [schema object definitions][1] documentation. - -.PARAMETER ValidateOnly - -If set, the cmdlet will stop at the first invalid node and return the test result object. - -.PARAMETER Elaborate - -If set, the cmdlet will return the test result object for all tested nodes, even if they are valid -or ruled out in a possible list node branch selection. - -.PARAMETER AssertTestPrefix - -The prefix used to identify the assert test nodes in the schema object. By default, the prefix is `AssertTestPrefix`. - -.PARAMETER MaxDepth - -The maximal depth to recursively test each embedded node. -The default value is defined by the PowerShell object node parser (`[PSNode]::DefaultMaxDepth`, default: `20`). - -.LINK - [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/SchemaObject.md "Schema object definitions" - -#> - -[Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] - $SchemaObject, - - [Parameter(ParameterSetName='ValidateOnly')] - [Switch]$ValidateOnly, - - [Parameter(ParameterSetName='ResultList')] - [Switch]$Elaborate, - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [ValidateNotNullOrEmpty()][String]$AssertTestPrefix = 'AssertTestPrefix', - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - - $Script:Yield = { - $Name = "$Args" -Replace '\W' - $Value = Get-Variable -Name $Name -ValueOnly -ErrorAction SilentlyContinue - if ($Value) { "$args" } - } - - $Script:Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } - - # The maximum schema object depth is bound by the input object depth (+1 one for the leaf test definition) - $SchemaNode = [PSNode]::ParseInput($SchemaObject, ($MaxDepth + 2)) # +2 to be safe - $Script:AssertPrefix = if ($SchemaNode.Contains($AssertTestPrefix)) { $SchemaNode.Value[$AssertTestPrefix] } else { '@' } - - function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } - - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$([String]$SchemaNode) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object - } - - $Script:Tests = @{ - Description = 'Describes the test node' - References = 'Contains a list of assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Required = 'The node is required' - Unique = 'The node is unique' - - Minimum = 'The value is greater than or equal to' - ExclusiveMinimum = 'The value is greater than' - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - - MinimumLength = 'The value length is greater than or equal to' - Length = 'The value length is equal to' - MaximumLength = 'The value length is less than or equal to' - - MinimumCount = 'The node count is greater than or equal to' - Count = 'The node count is equal to' - MaximumCount = 'The node count is less than or equal to' - - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' - - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - AllowExtraNodes = 'Allow extra nodes' - } - - $At = @{} - $Tests.Get_Keys().Foreach{ $At[$_] = "$($AssertPrefix)$_" } - - function ResolveReferences($Node) { - if ($Node.Cache.ContainsKey('TestReferences')) { return } - - } - - function GetReference($LeafNode) { - $TestNode = $LeafNode.ParentNode - $References = if ($TestNode) { - if (-not $TestNode.Cache.ContainsKey('TestReferences')) { - $Stack = [Stack]::new() - while ($true) { - $ParentNode = $TestNode.ParentNode - if ($ParentNode -and -not $ParentNode.Cache.ContainsKey('TestReferences')) { - $Stack.Push($TestNode) - $TestNode = $ParentNode - continue - } - $RefNode = if ($TestNode.Contains($At.References)) { $TestNode.GetChildNode($At.References) } - $TestNode.Cache['TestReferences'] = [HashTable]::new($Ordinal[[Bool]$RefNode.CaseMatters]) - if ($RefNode) { - foreach ($ChildNode in $RefNode.ChildNodes) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($ChildNode.Name)) { - $TestNode.Cache['TestReferences'][$ChildNode.Name] = $ChildNode - } - } - } - $ParentNode = $TestNode.ParentNode - if ($ParentNode) { - foreach ($RefName in $ParentNode.Cache['TestReferences'].get_Keys()) { - if (-not $TestNode.Cache['TestReferences'].ContainsKey($RefName)) { - $TestNode.Cache['TestReferences'][$RefName] = $ParentNode.Cache['TestReferences'][$RefName] - } - } - } - if ($Stack.Count -eq 0) { break } - $TestNode = $Stack.Pop() - } - } - $TestNode.Cache['TestReferences'] - } else { @{} } - if ($References.Contains($LeafNode.Value)) { - $AssertNode.Cache['TestReferences'] = $References - $References[$LeafNode.Value] - } - else { SchemaError "Unknown reference: $LeafNode" $ObjectNode $LeafNode } - } - - function MatchNode ( - [PSNode]$ObjectNode, - [PSNode]$TestNode, - [Switch]$ValidateOnly, - [Switch]$Elaborate, - [Switch]$Ordered, - [Nullable[Bool]]$CaseSensitive, - [Switch]$MatchAll, - $MatchedNames - ) { - $Violates = $null - $Name = $TestNode.Name - - $ChildNodes = $ObjectNode.ChildNodes - if ($ChildNodes.Count -eq 0) { return } - - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } - - if ($ObjectNode -is [PSMapNode] -and $TestNode.NodeOrigin -eq 'Map') { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "The node $Name is not in order" - } - } else { $ChildNode = $false } - } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Expected at least $($TestNodes.Count) (ordered) nodes" - } - $ChildNode = $ChildNodes[$NodeIndex] - } - else { $ChildNode = $null } - - if ($Violates) { - if (-not $ValidateOnly) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $AssertNode - Valid = -not $Violates - Issue = $Violates - } - $Output.PSTypeNames.Insert(0, 'TestResult') - $Output - } - return - } - if ($ChildNode -is [PSNode]) { - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Issue - } - TestNode @TestParams - if (-not $Issue) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - $SingleIssue = $Null - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Issue = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Issue - } - TestNode @TestParams - if($Issue) { - if ($Elaborate) { $Issue } - elseif (-not $ValidateOnly -and $MatchAll) { - if ($null -eq $SingleIssue) { $SingleIssue = $Issue } else { $SingleIssue = $false } - } - } - else { - $null = $MatchedNames.Add($ChildNode.Name) - if (-not $MatchAll) { break } - } - } - if ($SingleIssue) { $SingleIssue } - } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } - } - - function TestNode ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$Elaborate, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity frm the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node - ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ParameterColor]'Caller (line: $($Caller.ScriptLineNumber))'):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ParameterColor]'ObjectNode:')" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ParameterColor]'SchemaNode:')" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ParameterColor]'ValidateOnly:')" ([Bool]$ValidateOnly) - } - if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # Allow any node - - $AssertValue = $ObjectNode.Value - $RefInvalidNode.Value = $null - - # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} # $AssertNodes{] = $ChildNodes.@ - if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() - foreach ($Node in $SchemaNode.ChildNodes) { - if ($Null -eq $Node.Parent -and $Node.Name -eq $AssertTestPrefix) { continue } - if ($Node.Name.StartsWith($AssertPrefix)) { - $TestName = $Node.Name.SubString($AssertPrefix.Length) - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown assert: '$($Node.Name)'" $ObjectNode $SchemaNode } - $AssertNodes[$TestName] = $Node - } - else { $TestNodes.Add($Node) } - } - } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - if ($AssertNodes.Contains('CaseSensitive')) { $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] } - $AllowExtraNodes = if ($AssertNodes.Contains('AllowExtraNodes')) { $AssertNodes['AllowExtraNodes'] } - -#Region Node validation - - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null - foreach ($TestName in $AssertNodes.get_Keys()) { - $AssertNode = $AssertNodes[$TestName] - $Criteria = $AssertNode.Value - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Description') { $Null } - elseif ($TestName -eq 'References') { } - elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { - if ($TypeName -in $null, 'Null', 'Void') { - if ($null -eq $AssertValue) { $true; break } - } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } - } - if ($ObjectNode -is $Type -or $AssertValue -is $Type) { $true; break } - } - $Not = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - if ($null -eq $FoundType -xor $Not) { $Violates = "The node or value is $(if (!$Not) { 'not ' })of type $AssertNode" } - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "The case sensitivity value should be a boolean: $Criteria" $ObjectNode $SchemaNode - } - } - elseif ($TestName -in 'Minimum', 'ExclusiveMinimum', 'ExclusiveMaximum', 'Maximum') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } - $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } - foreach ($ValueNode in $ValueNodes) { - $Value = $ValueNode.Value - if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" - } - elseif ($TestName -eq 'Minimum') { - $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } - if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less or equal than $AssertNode" - } - } - elseif ($TestName -eq 'ExclusiveMinimum') { - $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } - if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is less than $AssertNode" - } - } - elseif ($TestName -eq 'ExclusiveMaximum') { - $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } - if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" - } - } - else { # if ($TestName -eq 'Maximum') { - $IsValid = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } - if (-not $IsValid) { - $Violates = "The $(&$Yield '(case sensitive) ')value $Value is greater than $AssertNode" - } - } - if ($Violates) { break } - } - } - - elseif ($TestName -in 'MinimumLength', 'Length', 'MaximumLength') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } - $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } - foreach ($ValueNode in $ValueNodes) { - $Value = $ValueNode.Value - if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" - break - } - $Length = "$Value".Length - if ($TestName -eq 'MinimumLength') { - if ($Length -lt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is less than $AssertNode" - } - } - elseif ($TestName -eq 'Length') { - if ($Length -ne $Criteria) { - $Violates = "The string length of '$Value' ($Length) is not equal to $AssertNode" - } - } - else { # if ($TestName -eq 'MaximumLength') { - if ($Length -gt $Criteria) { - $Violates = "The string length of '$Value' ($Length) is greater than $AssertNode" - } - } - if ($Violates) { break } - } - } - - elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - if ($null -eq $AllowExtraNodes) { $AllowExtraNodes = $true } - $Negate = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - $Match = $TestName.EndsWith('Match', 'OrdinalIgnoreCase') - $ValueNodes = if ($ObjectNode -is [PSCollectionNode]) { $ObjectNode.ChildNodes } else { @($ObjectNode) } - foreach ($ValueNode in $ValueNodes) { - $Value = $ValueNode.Value - if ($Value -isnot [String] -and $Value -isnot [ValueType]) { - $Violates = "The value '$Value' is not a string or value type" - break - } - $Found = $false - foreach ($AnyCriteria in $Criteria) { - $Found = if ($Match) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } - } - else { # if ($TestName.EndsWith('Link', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } - } - if ($Found) { break } - } - $IsValid = $Found -xor $Negate - if (-not $IsValid) { - $Not = if (-Not $Negate) { ' not' } - $Violates = - if ($Match) { "The $(&$Yield '(case sensitive) ')value $Value does$not match $AssertNode" } - else { "The $(&$Yield '(case sensitive) ')value $Value is$not like $AssertNode" } - } - } - } - - elseif ($TestName -in 'MinimumCount', 'Count', 'MaximumCount') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The node $ObjectNode is not a collection node" - } - elseif ($TestName -eq 'MinimumCount') { - if ($ChildNodes.Count -lt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is less than $AssertNode" - } - } - elseif ($TestName -eq 'Count') { - if ($ChildNodes.Count -ne $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is not equal to $AssertNode" - } - } - else { # if ($TestName -eq 'MaximumCount') { - if ($ChildNodes.Count -gt $Criteria) { - $Violates = "The node count ($($ChildNodes.Count)) is greater than $AssertNode" - } - } - } - - elseif ($TestName -eq 'Required') { } - elseif ($TestName -eq 'Unique' -and $Criteria) { - if (-not $ObjectNode.ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode - } - if ($Criteria -eq $true) { $UniqueCollection = $ObjectNode.ParentNode.ChildNodes } - elseif ($Criteria -is [String]) { - if (-not $UniqueCollections.Contains($Criteria)) { - $UniqueCollections[$Criteria] = [List[PSNode]]::new() - } - $UniqueCollection = $UniqueCollections[$Criteria] - } - else { SchemaError "The unique assert value should be a boolean or a string" $ObjectNode $SchemaNode } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) - foreach ($UniqueNode in $UniqueCollection) { - if ([object]::ReferenceEquals($ObjectNode, $UniqueNode)) { continue } # Self - if ($ObjectComparer.IsEqual($ObjectNode, $UniqueNode)) { - $Violates = "The node is equal to the node: $($UniqueNode.Path)" - break - } - } - if ($Criteria -is [String]) { $UniqueCollection.Add($ObjectNode) } - } - elseif ($TestName -eq 'AllowExtraNodes') {} - elseif ($TestName -in 'Ordered', 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' is not a collection node" - } - } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } - - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } - - if ($Violates -or $Elaborate) { - $Issue = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName]) } - else { "$($Tests[$TestName] -replace 'The value ', "The value $ObjectNode ") $AssertNode" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = $Issue - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } - } - } - -#EndRegion Node validation - - if ($Violates) { return } - -#Region Required nodes - - $ChildNodes = $ObjectNode.ChildNodes - - if ($TestNodes.Count -and -not $AssertNodes.Contains('Type')) { - if ($SchemaNode -is [PSListNode] -and $ObjectNode -isnot [PSListNode]) { - $Violates = "The node $ObjectNode is not a list node" - } - if ($SchemaNode -is [PSMapNode] -and $ObjectNode -isnot [PSMapNode]) { - $Violates = "The node $ObjectNode is not a map node" - } - } - - if (-Not $Violates) { - $RequiredNodes = $AssertNodes['RequiredNodes'] - $CaseSensitiveNames = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.CaseMatters } - $AssertResults = [HashTable]::new($Ordinal[[Bool]$CaseSensitiveNames]) - - if ($RequiredNodes) { $RequiredList = [List[Object]]$RequiredNodes.Value } else { $RequiredList = [List[Object]]::new() } - foreach ($TestNode in $TestNodes) { - $AssertNode = if ($TestNode -is [PSCollectionNode]) { $TestNode } else { GetReference $TestNode } - if ($AssertNode -is [PSMapNode] -and $AssertNode.GetValue($At.Required)) { $RequiredList.Add($TestNode.Name) } - } - - foreach ($Requirement in $RequiredList) { - $LogicalFormula = [LogicalFormula]$Requirement - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $null - Operator = $null - Negate = $null - }) - $Term, $Operand, $Accumulator = $null - While ($Stack.Count -gt 0) { - # Accumulator = Accumulator Operand - # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Operator = $Pop.Operator - if ($null -eq $Operator) { $Operand = $Pop.Accumulator } - else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } - $Negate = $Pop.Negate - $Compute = $null -notin $Operand, $Operator, $Accumulator - while ($Compute -or $Enumerator.MoveNext()) { - if ($Compute) { $Compute = $false} - else { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $SchemaNode.GetChildNode($Name) - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = $false - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - $AssertResults[$Name] = $MatchedNames.Count -gt $MatchCount0 - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term.Value -eq 'Not') { $Negate = -Not $Negate } - elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } - else { SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode } - } - elseif ($Term -is [LogicalFormula]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator, $Operator, $Negate = $null - $Enumerator = $Term.Terms.GetEnumerator() - continue - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - $Negate = $null - if ($Operator -eq 'And') { - $Operator = $null - if ($Accumulator -eq $false -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - $Operator = $null - if ($Accumulator -eq $true -and -not $AllowExtraNodes) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Operator = $null - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand = $Null - } - } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Operator" $ObjectNode $SchemaNode - } - } - if ($Accumulator -eq $False) { - $Violates = "The required node condition $LogicalFormula is not met" - break - } - } - } - -#EndRegion Required nodes - -#Region Optional nodes - - if (-not $Violates) { - - foreach ($TestNode in $TestNodes) { - if ($MatchedNames.Count -ge $ChildNodes.Count) { break } - if ($AssertResults.Contains($TestNode.Name)) { continue } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $TestNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = -not $AllowExtraNodes - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - if ($AllowExtraNodes -and $MatchedNames.Count -eq $MatchCount0) { - $Violates = "When extra nodes are allowed, the node $ObjectNode should be accepted" - break - } - $AssertResults[$TestNode.Name] = $MatchedNames.Count -gt $MatchCount0 - } - - if (-not $AllowExtraNodes -and $MatchedNames.Count -lt $ChildNodes.Count) { - $Count = 0; $LastName = $Null - $Names = foreach ($Name in $ChildNodes.Name) { - if ($MatchedNames.Contains($Name)) { continue } - if ($Count++ -lt 4) { - if ($ObjectNode -is [PSListNode]) { [CommandColor]$Name } - else { [StringColor][PSKeyExpression]::new($Name, [PSSerialize]::MaxKeyLength)} - } - else { $LastName = $Name } - } - $Violates = "The following nodes are not accepted: $($Names -join ', ')" - if ($LastName) { - $LastName = if ($ObjectNode -is [PSListNode]) { [CommandColor]$LastName } - else { [StringColor][PSKeyExpression]::new($LastName, [PSSerialize]::MaxKeyLength) } - $Violates += " .. $LastName" - } - } - } - -#EndRegion Optional nodes - - if ($Violates -or $Elaborate) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Issue = if ($Violates) { $Violates } else { 'All the child nodes are valid'} - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { $RefInvalidNode.Value = $Output } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } - } - } -} - -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Script:UniqueCollections = @{} - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestNode @TestParams - if ($ValidateOnly) { -not $Invalid } -} -} - -#EndRegion Cmdlet - -#Region Alias - -Set-Alias -Name 'ConvertFrom-Expression' -Value 'cfe' -Set-Alias -Name 'Copy-ObjectGraph' -Value 'Copy-Object' -Set-Alias -Name 'Copy-ObjectGraph' -Value 'cpo' -Set-Alias -Name 'ConvertTo-Expression' -Value 'cto' -Set-Alias -Name 'Export-ObjectGraph' -Value 'epo' -Set-Alias -Name 'Export-ObjectGraph' -Value 'Export-Object' -Set-Alias -Name 'Get-ChildNode' -Value 'gcn' -Set-Alias -Name 'Get-Node' -Value 'gn' -Set-Alias -Name 'Import-ObjectGraph' -Value 'imo' -Set-Alias -Name 'Import-ObjectGraph' -Value 'Import-Object' -Set-Alias -Name 'Merge-ObjectGraph' -Value 'Merge-Object' -Set-Alias -Name 'Merge-ObjectGraph' -Value 'mgo' -Set-Alias -Name 'Get-SortObjectGraph' -Value 'Sort-ObjectGraph' -Set-Alias -Name 'Get-SortObjectGraph' -Value 'sro' -Set-Alias -Name 'Test-ObjectGraph' -Value 'Test-Object' -Set-Alias -Name 'Test-ObjectGraph' -Value 'tso' - -#EndRegion Alias - -#Region Format - -if (-not (Get-FormatData 'PSNode' -ErrorAction Ignore)) { - Update-FormatData -PrependPath $PSScriptRoot\Source\Formats\PSNode.Format.ps1xml -} -if (-not (Get-FormatData 'TestResult' -ErrorAction Ignore)) { - Update-FormatData -PrependPath $PSScriptRoot\Source\Formats\TestResultTable.Format.ps1xml -} -if (-not (Get-FormatData 'XdnName' -ErrorAction Ignore)) { - Update-FormatData -PrependPath $PSScriptRoot\Source\Formats\XdnName.Format.ps1xml -} -if (-not (Get-FormatData 'XdnPath' -ErrorAction Ignore)) { - Update-FormatData -PrependPath $PSScriptRoot\Source\Formats\XdnPath.Format.ps1xml -} - -#EndRegion Format - -#Region Export - -$ModuleMembers = @{ - Function = 'Compare-ObjectGraph', 'ConvertFrom-Expression', 'ConvertTo-Expression', 'Copy-ObjectGraph', 'Export-ObjectGraph', 'Get-ChildNode', 'Get-Node', 'Get-SortObjectGraph', 'Import-ObjectGraph', 'Merge-ObjectGraph', 'Test-ObjectGraph' - Alias = 'cfe', 'cto', 'Copy-Object', 'cpo', 'Export-Object', 'epo', 'gcn', 'gn', 'Sort-ObjectGraph', 'sro', 'Import-Object', 'imo', 'Merge-Object', 'mgo', 'Test-Object', 'tso' -} -Export-ModuleMember @ModuleMembers -# https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#exporting-classes-with-type-accelerators -# Define the types to export with type accelerators. -$ExportableTypes = @( - [LogicalOperatorEnum] - [PSNodeStructure] - [PSNodeOrigin] - [ObjectCompareMode] - [ObjectComparison] - [XdnType] - [XdnColorName] - [Abbreviate] - [LogicalTerm] - [LogicalOperator] - [LogicalVariable] - [LogicalFormula] - [PSNodePath] - [PSNode] - [PSLeafNode] - [PSCollectionNode] - [PSListNode] - [PSMapNode] - [PSDictionaryNode] - [PSObjectNode] - [ObjectComparer] - [PSListNodeComparer] - [PSMapNodeComparer] - [PSDeserialize] - [PSInstance] - [PSKeyExpression] - [PSLanguageType] - [PSSerialize] - [ANSI] - [TextStyle] - [TextColor] - [CommandColor] - [CommentColor] - [ContinuationPromptColor] - [DefaultTokenColor] - [EmphasisColor] - [ErrorColor] - [KeywordColor] - [MemberColor] - [NumberColor] - [OperatorColor] - [ParameterColor] - [SelectionColor] - [StringColor] - [TypeColor] - [VariableColor] - [InverseColor] - [XdnName] - [XdnPath] -) - -$TypeAcceleratorsClass = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') - -foreach ($Type in $ExportableTypes) { - if ($Type.FullName -notin $ExistingTypeAccelerators.Keys) { - $TypeAcceleratorsClass::Add($Type.FullName, $Type) - } -} - -$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { - foreach($Type in $ExportableTypes) { - $TypeAcceleratorsClass::Remove($Type.FullName) - } -}.GetNewClosure() - -#EndRegion Export diff --git a/_Temp/RecursiveDictionary.ps1 b/_Temp/RecursiveDictionary.ps1 deleted file mode 100644 index d9a3754..0000000 --- a/_Temp/RecursiveDictionary.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -using namespace system.collections -using namespace system.collections.generic - -class RecursiveDictionary : IDictionary { - - - -} \ No newline at end of file diff --git a/_Temp/TablePester.ps1 b/_Temp/TablePester.ps1 deleted file mode 100644 index 471a6cb..0000000 --- a/_Temp/TablePester.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Build-TableShouldBe ($InputObject) { - $Lines = ($InputObject | Format-Table -Auto | Out-String -Stream).TrimEnd().where{ $_ } - $Digits = ($Lines.Count - 1).ToString().Length - for ($Index = 0; $Index -lt $Lines.Count; $Index++) { - "`$Lines[{0:d$Digits}] | Should -be '{1}'" -f $Index, $Lines[$Index] - } -} - -# $Service = Get-Service | Select-Object -First 1 -# Build-TableShouldBe $Service diff --git a/_Temp/Test-CaseSensitive.ps1 b/_Temp/Test-CaseSensitive.ps1 deleted file mode 100644 index acdac2d..0000000 --- a/_Temp/Test-CaseSensitive.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -using namespace System.Collections -using namespace System.Collections.Generic - -class TestClass { - hidden [String] $_Value - TestClass([String]$Value) { $this._Value = $Value } - [Bool] Equals($Test, [StringComparison]$StringComparison) { - return $this._Value.Equals([String]$Test._Value, $StringComparison) - } - [Bool] Equals($Test) { - return $this.Equals($Test, [StringComparison]::CurrentCultureIgnoreCase) - } -} - -$a1Lower = [TestClass]'a' -$b1Lower = [TestClass]'b' -$a2Lower = [TestClass]'a' -$a2Upper = [TestClass]'A' - -$a1Lower -eq $b1Lower # False -$a1Lower -eq $a2Lower # True -$a1Lower -eq $a2Upper # True - -$a1Lower -ceq $a2Lower # True -$a1Lower -ceq $a2Upper # True (expected false) diff --git a/_Temp/Test-ObjectGraph copy1.ps1 b/_Temp/Test-ObjectGraph copy1.ps1 deleted file mode 100644 index 7103ca6..0000000 --- a/_Temp/Test-ObjectGraph copy1.ps1 +++ /dev/null @@ -1,467 +0,0 @@ -using module .\..\..\ObjectGraphTools.psm1 - -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Collections -using namespace System.Collections.Generic - -<# -.SYNOPSIS - Tests the properties of an object-graph. - -.DESCRIPTION - Tests an object-graph against a schema object by verifying that the properties of the object-graph - meet the constrains defined in the schema object. - - Statements: - * Requires defines the test order - -#> - -[Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] - $SchemaObject, - - [Parameter(ParameterSetName='ValidateOnly')] - [Switch]$ValidateOnly, - - [Parameter(ParameterSetName='ResultList')] - [Alias('All')][Switch]$IncludeAll, - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - $AssertPrefix = '@', - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - -# JsonSchema Properties -# Schema properties: [NewtonSoft.Json.Schema.JsonSchema]::New() | Get-Member -# https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Schema_JsonSchema.htm - - - Enum UniqueType { None; Node; Match } # if a node isn't unique the related option isn't uniquely matched either - Enum CompareType { Scalar; OneOf; AllOf } - - $Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } - - function StopError($Exception, $Id = 'TestObject', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } - - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$($SchemaNode.Synopsys) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object - } - - $LimitTests = [Ordered]@{ - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - ExclusiveMinimum = 'The value is greater than' - Minimum = 'The value is greater than or equal to' - } - - $MatchTests = [Ordered]@{ - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' - } - - $Tests = [Ordered]@{ - Title = 'Title' - References = 'Assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Unique = 'The node is unique' - MatchAll = 'Match all the nodes' - } + - $LimitTests + - $MatchTests + - [Ordered]@{ - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - DenyExtraNodes = 'There no additional nodes left over' - } - - function TestObject ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$IncludeAll, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity from the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node - ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ANSI]::ParameterColor)Caller (line: $($Caller.ScriptLineNumber))$([ANSI]::ResetColor):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ANSI]::ParameterColor)ObjectNode:$([ANSI]::ResetColor)" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ANSI]::ParameterColor)SchemaNode:$([ANSI]::ResetColor)" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ANSI]::ParameterColor)ValidOnly:$([ANSI]::ResetColor)" ([Bool]$ValidateOnly) - } - - $Value = $ObjectNode.Value - $RefInvalidNode.Value = $null - - # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} - if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() - foreach ($Node in $SchemaNode.ChildNodes) { - if ($Node.Name.StartsWith($AssertPrefix)) { $AssertNodes[$Node.Name.SubString(1)] = $Node.Value } - else { $TestNodes.Add($Node) } - } - } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - # Define the required nodes if not already defined - if (-not $AssertNodes.Contains('RequiredNodes') -and $ObjectNode -is [PSCollectionNode]) { - $AssertNodes['RequiredNodes'] = $TestNodes.Name - } - - if ($AssertNodes.Contains('CaseSensitive')) { - $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] - } - - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null - foreach ($TestName in $Tests.Keys) { - if ($TestName -notin $AssertNodes.Keys) { continue } - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown test name: $TestName" $ObjectNode $SchemaNode } - $Criteria = $AssertNodes[$TestName] - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Title') { $Null } - elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { - if ($TypeName -in $null, 'Null', 'Void') { - if ($null -eq $Value) { $true; break } - } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } - } - if ($ObjectNode -is $Type -or $Value -is $Type) { $true; break } - } - $Violates = $null -eq $FoundType -xor $TestName -eq 'notType' - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "Invalid case sensitivity value: $Criteria" $ObjectNode $SchemaNode - } - } - elseif ($TestName -eq 'ExclusiveMinimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } - } - elseif ($TestName -eq 'Minimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } - } - elseif ($TestName -eq 'ExclusiveMaximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } - } - elseif ($TestName -eq 'Maximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } - } - - elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - $Match = foreach ($AnyCriteria in $Criteria) { - $IsMatch = if ($TestName.EndsWith('Like', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } - } - else { # if ($TestName.EndsWith('Match', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } - } - if ($IsMatch) { $true; break } - } - $Violates = -not $Match -xor $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - } - - elseif ($TestName -eq 'Unique') { - $ParentNode = $ObjectNode.ParentNode - if (-not $ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode - } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) - foreach ($SiblingNode in $ParentNode.ChildNodes) { - if ($ObjectNode.Name -ceq $SiblingNode.Name) { continue } # Self - if ($ObjectComparer.IsEqual($ObjectNode, $SiblingNode)) { - $Violates = $true - break - } - } - } - elseif ($TestName -eq 'MatchAll') { # the assert exclusivity is handled by the parent node - $ParentNode = $ObjectNode.ParentNode - if (-not $ParentNode) { - SchemaError "The MatchAll assert can't be used on a root node" $ObjectNode $SchemaNode - if ($ParentNode.GetValue('@Ordered')) { - SchemaError "The MatchAll assert can't be used on an ordered node" $ObjectNode $SchemaNode - } - } - } - elseif ($TestName -eq 'Ordered') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = 'The ordered assert requires a collection node' - } - } - - elseif ($TestName -eq 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = 'The requires assert requires a collection node' - } - else { - $ChildNodes = $ObjectNode.ChildNodes - $IsStrictCase = if ($ObjectNode -is [PSMapNode]) { - foreach ($ChildNode in $ChildNodes) { - $Name = $ChildNode.Name - $IsStrictCase = if ($Name -is [String] -and $Name -match '[a-z]') { - $Case = $Name.ToLower() - if ($Case -eq $Name) { $Case = $Name.ToUpper() } - -not $ObjectNode.Contains($Case) -or $ObjectNode.GetChildNode($Case).Name -ceq $Case - break - } - } - } elseif ($ObjectNode -is [PSCollectionNode]) { $false } else { $null } - - $AssertResults = [HashTable]::new($Ordinal[[Bool]$IsStrictCase]) - foreach ($Condition in $Criteria) { - $Term, $Accumulator, $Operand, $Operation, $Negate = $null - $LogicalFormula = [LogicalFormula]$Condition - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [System.Collections.Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Null - Operator = $Null - Negate = $Null - }) - $Accumulator = $Null - While ($Stack.Count -gt 0) { # Accumulator = Accumulator Operand - if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Operand = $Accumulator # Resulted from sub expression - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Accumulator = $Pop.Accumulator - $Operator = $Pop.Operator - $Negate = $Pop.Negate - while ($Enumerator.MoveNext()) { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $TestNode = $SchemaNode.GetChildNode($Name) - $ChildNode = $null - if ($ChildNodes.Count -eq 0) { $AssertResults[$Name] = $false } - elseif ($ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode]) { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "Node $Name should be in order" - $Stack.Clear() - break - } - } else { $ChildNode = $false } - } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Should contain at least $($TestNodes.Count) nodes" - $Stack.Clear() - break - } - $ChildNode = $ChildNodes[$NodeIndex] - } - - if ($ChildNode -is [PSNode]) { - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $TestNode - IncludeAll = $IncludeAll - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestObject @TestParams - $AssertResults[$Name] = -not $Invalid - if (-not $Invalid) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - $Violates = $null - $MatchAll = $false - $FoundMatch = $false - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $TestNode - IncludeAll = $IncludeAll - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Invalid - } - TestObject @TestParams - if ($Invalid) { - if ($IncludeAll) { <# Write-Output #> $Invalid } - continue - } - else { - $FoundMatch = $true - $null = $MatchedNames.Add($ChildNode.Name) - if ($TestNode.GetValue('@MatchAll')) { $MatchAll = $true } - if (-not $MatchAll) { break } - } - } - $AssertResults[$ChildNode.Name] = $FoundMatch - } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term -eq 'Not') { $Negate = -Not $Negate } - if ( - $null -ne $Operation -or $null -eq $Accumulator) { - SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode - } - - } - elseif ($Term -is [List[Object]]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator = $null - $Enumerator = $Term.GetEnumerator() - break - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - if ($Operator -eq 'And') { - if ($Accumulator -eq $false) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - if ($Accumulator -eq $true) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand, $Operator, $Negate = $Null - } - } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Term" $ObjectNode $SchemaNode - } - } - if ($Accumulator -eq $False) { - $Violates = "Meets the conditions of the nodes $LogicalFormula" - if ($ValidateOnly) { break } - } - } - } - } - elseif ($AssertNodes['DenyExtraNodes']) { - if ($MatchedNames.Count -lt $ChildNodes.Count) { - $Extra = $ChildNodes.Name.where{ -not $MatchedNames.Contains($_) }.foreach{ [PSSerialize]$_ } -Join ', ' - $Violates = "Deny the extra node(s): $Extra" - } - } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } - - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } - - if ($Violates -or $IncludeAll) { - $Condition = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName])} - else { "$($Tests[$TestName]) $(@($Criteria).foreach{ [PSSerialize]$_ } -Join ', ')" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $IncludeAll) { <# Write-Output #> $Output } - } - } - } - - $SchemaNode = [PSNode]::ParseInput($SchemaObject) -} - -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - IncludeAll = $IncludeAll - ValidateOnly = $ValidateOnly - CaseSensitive = $CaseSensitive - RefInvalidNode = [Ref]$Invalid - } - TestObject @TestParams - if ($ValidateOnly) { -not $Invalid } -} - diff --git a/_Temp/Test-ObjectGraph2.ps1 b/_Temp/Test-ObjectGraph2.ps1 deleted file mode 100644 index 2e57531..0000000 --- a/_Temp/Test-ObjectGraph2.ps1 +++ /dev/null @@ -1,526 +0,0 @@ -using module .\..\..\ObjectGraphTools.psm1 - -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Collections -using namespace System.Collections.Generic - -<# -.SYNOPSIS - Tests the properties of an object-graph. - -.DESCRIPTION - Tests an object-graph against a schema object by verifying that the properties of the object-graph - meet the constrains defined in the schema object. - -.EXAMPLE - # Parse a object graph to a node instance - - The following example parses a hash table to `[PSNode]` instance: - - @{ 'My' = 1, 2, 3; 'Object' = 'Graph' } | Get-Node - - PathName Name Depth Value - -------- ---- ----- ----- - 0 {My, Object} - -.PARAMETER InputObject - The input object that will be compared with the reference object (see: [-Reference] parameter). - -#> - -[Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] - $SchemaObject, - - [Parameter(ParameterSetName='ValidateOnly')] - [Switch]$ValidateOnly, - - [Parameter(ParameterSetName='ResultList')] - [Switch]$IncludeValid, - - [Parameter(ParameterSetName='ResultList')] - [Switch]$IncludeLatent, - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - $AssertPrefix = '@', - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - -# JsonSchema Properties -# Schema properties: [NewtonSoft.Json.Schema.JsonSchema]::New() | Get-Member -# https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Schema_JsonSchema.htm - - - Enum UniqueType { None; Node; Match } # if a node isn't unique the related option isn't uniquely matched either - Enum CompareType { Scalar; OneOf; AllOf } - - $Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } - - function StopError($Exception, $Id = 'TestObject', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } - - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$($SchemaNode.Synopsys) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object - } - - # $LimitTests = [Ordered]@{ - # ExclusiveMaximum = 'The value is less than' - # cExclusiveMaximum = 'The value is (case sensitive) less than' - # iExclusiveMaximum = 'The value is (case insensitive) less than' - # Maximum = 'The value is less than or equal to' - # cMaximum = 'The value is (case sensitive) less than or equal to' - # iMaximum = 'The value is (case insensitive) less than or equal to' - # ExclusiveMinimum = 'The value is greater than' - # cExclusiveMinimum = 'The value is (case sensitive) greater than' - # iExclusiveMinimum = 'The value is (case insensitive) greater than' - # Minimum = 'The value is greater than or equal to' - # cMinimum = 'The value is (case sensitive) greater than or equal to' - # iMinimum = 'The value is (case insensitive) greater than or equal to' - # } - - # $MatchTests = [Ordered]@{ - # Like = 'The value is like' - # iLike = 'The value is (case insensitive) like' - # cLike = 'The value is (case sensitive) like' - # Match = 'The value matches' - # iMatch = 'The value (case insensitive) matches' - # cMatch = 'The value (case sensitive) matches' - # NotLike = 'The value is not like' - # iNotLike = 'The value is not (case insensitive) like' - # cNotLike = 'The value is not (case sensitive) like' - # NotMatch = 'The value not matches' - # iNotMatch = 'The value not (case insensitive) matches' - # cNotMatch = 'The value not (case sensitive) matches' - # } - - $LimitTests = [Ordered]@{ - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - ExclusiveMinimum = 'The value is greater than' - Minimum = 'The value is greater than or equal to' - } - - $MatchTests = [Ordered]@{ - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' - } - - $Tests = [Ordered]@{ - Title = 'Title' - References = 'Assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Unique = 'The node is unique' - Exclusive = 'The assert is exclusive' - } + - $LimitTests + - $MatchTests + - [Ordered]@{ - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - DenyExtraNodes = 'There no additional nodes left over' - } - - function TestObject ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$IncludeValid, # if set, include the valid test results in the output - [Switch]$IncludeLatent, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity from the parent node if not defined - [Switch]$ValidateOnly, - $RefValid - ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ANSI]::ParameterColor)Caller (line: $($Caller.ScriptLineNumber))$([ANSI]::ResetColor):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ANSI]::ParameterColor)ObjectNode:$([ANSI]::ResetColor)" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ANSI]::ParameterColor)SchemaNode:$([ANSI]::ResetColor)" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ANSI]::ParameterColor)ValidOnly:$([ANSI]::ResetColor)" ([Bool]$ValidateOnly) - } - - $Value = $ObjectNode.Value - - # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} - if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() - foreach ($Node in $SchemaNode.ChildNodes) { - if ($Node.Name.StartsWith($AssertPrefix)) { $AssertNodes[$Node.Name.SubString(1)] = $Node.Value } - else { $TestNodes.Add($Node) } - } - } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - # Define the required nodes if not already defined - if (-not $AssertNodes.Contains('RequiredNodes') -and $ObjectNode -is [PSCollectionNode]) { - $AssertNodes['RequiredNodes'] = $TestNodes.Name - } - - if ($AssertNodes.Contains('CaseSensitive')) { - $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] - } - $DenyExtraNodes = $AssertNodes['DenyExtraNodes'] - $Ordered = $AssertNodes['Ordered'] - - $RefValid.Value = $true - $ChildNodes = $AssertResults = $Null - foreach ($TestName in $Tests.Keys) { - if ($TestName -notin $AssertNodes.Keys) { continue } - - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown test name: $TestName" $ObjectNode $SchemaNode } - $Criteria = $AssertNodes[$TestName] - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Title') { $Null } - elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { - if ($TypeName -in $null, 'Null', 'Void' -and $null -eq $Value) { $true; break } - if ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } - } - if ($ObjectNode -is $Type -or $Value -is $Type) { $true; break } - } - $Violates = $null -eq $FoundType -xor $TestName -eq 'notType' - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "Invalid case sensitivity value: $Criteria" $ObjectNode $SchemaNode - } - } - elseif ($TestName -eq 'ExclusiveMinimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } - } - elseif ($TestName -eq 'Minimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } - } - elseif ($TestName -eq 'ExclusiveMaximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } - } - elseif ($TestName -eq 'Maximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } - } - - elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - $Match = foreach ($AnyCriteria in $Criteria) { - $IsMatch = if ($TestName.EndsWith('Like', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } - } - else { # if ($TestName.EndsWith('Match', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } - } - if ($IsMatch) { $true; break } - } - $Violates = -not $Match -xor $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - } - - elseif ($TestName -eq 'Unique') { - $ParentNode = $ObjectNode.ParentNode - if ($ParentNode -isnot [PSCollectionNode]) { - $Violates = 'The unique assert requires a child node' - } - else { - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) - foreach ($SiblingNode in $ParentNode.ChildNodes) { - if ($ObjectNode.Name -ceq $SiblingNode.Name) { continue } # Self - if ($ObjectComparer.IsEqual($ObjectNode, $SiblingNode)) { - $Violates = $true - break - } - } - } - } - elseif ($TestName -eq 'Exclusive') { # the assert exclusivity is handled by the parent node - if ($ObjectNode.ParentNode -isnot [PSCollectionNode]) { - $Violates = 'The exclusive assert requires a collection item' - } - } - elseif ($TestName -eq 'Ordered') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = 'The ordered assert requires a collection node' - } - } - - elseif ($TestName -eq 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = 'The requires assert requires a collection node' - } - else { - $ChildNodes = $ObjectNode.ChildNodes - $IsStrictCase = if ($ObjectNode -is [PSMapNode]) { - foreach ($ChildNode in $ChildNodes) { - $Name = $ChildNode.Name - $IsStrictCase = if ($Name -is [String] -and $Name -match '[a-z]') { - $Case = $Name.ToLower() - if ($Case -eq $Name) { $Case = $Name.ToUpper() } - -not $ObjectNode.Contains($Case) -or $ObjectNode.GetChildNode($Case).Name -ceq $Case - break - } - } - } elseif ($ObjectNode -is [PSCollectionNode]) { $false } else { $null } - - $AssertResults = [HashTable]::new($Ordinal[[Bool]$IsStrictCase]) - $ValidNodes = [HashSet[Object]]::new() - foreach ($Condition in $Criteria) { - $Term, $Accumulator, $Operand, $Operation, $Negate = $null - $LogicalFormula = [LogicalFormula]$Condition - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [System.Collections.Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Null - Operator = $Null - Negate = $Null - }) - $Accumulator = $Null - While ($Stack.Count -gt 0) { # Accumulator = Accumulator Operand - if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Operand = $Accumulator # Resulted from sub expression - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Accumulator = $Pop.Accumulator - $Operator = $Pop.Operator - $Negate = $Pop.Negate - while ($Enumerator.MoveNext()) { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $TestNode = $SchemaNode.GetChildNode($Name) - $Name = $TestNode.Name # get the exact node name - $Mapped = $ObjectNode -is [PSMapNode] -and $SchemaNode -is [PSMapNode] - if ($Mapped -or $Ordered) { - $ChildNode = $Null - if ($Mapped) { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "Node $Name should be in order" - $Stack.Clear(); break - } - } - } - else { - $TestNodeIndex = $TestNodes.IndexOf($TestNode) - if ($TestNodeIndex -ge $ChildNodes.Count) { - $Violates = "Should contain at least $($TestNodes.Count) nodes" - $Stack.Clear(); break - } - $ChildNode = $ChildNodes[$NodeIndex] - } - if ($ChildNode) { - $Valid = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $TestNode - IncludeValid = $IncludeValid - IncludeLatent = $IncludeLatent - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefValid = [Ref]$Valid - } - TestObject @TestParams - $AssertResults[$Name] = $Valid - } - else { $AssertResults[$Name] = $false } - } - else { - $RestNames = $AssertNodes.where{ -not $AssertResults.Contains($_.Name) } - $UniqueNames = $RestNames.where{ $AssertNodes[$_].GetValue('@Unique') } - $ExclusiveNames = $RestNames.where{ $AssertNodes[$_].GetValue('@Exclusive') } - if ($UniqueNames -or $ExclusiveNames) { - $CheckNames = [List[String]]::new($UniqueNames) - $ExclusiveNames.foreach{ if ($_ -notin $CheckNames) { $CheckNames.Add($_) } } - $RestNames.foreach{ if ($_ -notin $CheckNames) { $CheckNames.Add($_) } } - } else { $CheckNames = $ChildNodes.Name } - $Found = $null - $CountUnique = if ($UniqueNames) { @{} } - $CountExclusive = if ($ExclusiveNames) { @{} } - $CountNodes = $CountUnique -or $CountExclusive - foreach ($ChildNode in $AssertNodes.get_Keys()) { - - if ($TestNode.GetValue('@Unique') -or $TestNode.GetValue('@Exclusive')) { continue } - $Valid = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $TestNode - IncludeValid = $IncludeValid - IncludeLatent = $IncludeLatent - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefValid = [Ref]$Valid - } - TestObject @TestParams - if ($Valid) { - if ($CountNodes) { - if ($CountUnique) { $CountUnique[$ChildNode] += 1 } - if ($CountExclusive) { $CountExclusive[$ChildNode] += 1 } - } else { $Found = $ChildNode; break } - } - } - if ($CountNodes) { - foreach ($CheckNode in $CheckNames) { - $Found = $CheckNode - if ($CountUnique -and $CountUnique[$CheckNode] -ne 1) { $Found = $null } - if ($CountExclusive -and $CountExclusive[$CheckNode] -ne 1) { $Found = $null } - if ($Found) { break } - } - } - if ($Found) { - $AssertResults[$Name] = $true - $null = $ValidNodes.Add($Found) - } else { $AssertResults[$Name] = $false } - } - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term -eq 'Not') { $Negate = -Not $Negate } - if ( - $null -ne $Operation -or $null -eq $Accumulator) { - SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode - } - - } - elseif ($Term -is [List[Object]]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator = $null - $Enumerator = $Term.GetEnumerator() - break - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - if ($Operator -eq 'And') { - if (-not $DenyExtraNodes -and $Accumulator -eq $false) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - if (-not $DenyExtraNodes -and $Accumulator -eq $true) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand, $Operator, $Negate = $Null - } - } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Term" $ObjectNode $SchemaNode - } - } - if ($Accumulator -eq $False) { - $Violates = "Meets the conditions of the nodes $LogicalFormula" - if ($ValidateOnly) { continue } - } - } - } - } - elseif ($AssertNodes['DenyExtraNodes']) { - $ChildNames = if ($ChildNodes) { $ChildNodes.Name } else { $ObjectNode.ChildNodes.Name } - $ResultNames = if ($AssertResults) { $AssertResults.get_Keys() } - if ($ResultNames.Count -lt $ChildNames.Count) { - $Extra = $ChildNames.where{ $ResultNames -cne $_ }.foreach{ [PSSerialize]$_ } -Join ', ' - $Violates = "Deny the extra node(s): $Extra" - } - } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } - - $RefValid.Value = -not $Violates - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if ($RefValid.Value) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } - - if ($IncludeLatent -or ($Violates -and -not $ValidateOnly) -or ($RefValid.Value -and $IncludeValid)) { - $Condition = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName])} - else { "$($Tests[$TestName]) $(@($Criteria).foreach{ [PSSerialize]$_ } -Join ', ')" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = $RefValid.Value - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResultTable') - Write-Output $Output - } - if ($ValidateOnly) { if ($RefValid.Value) { continue } else { return } } - } - } - - $SchemaNode = [PSNode]::ParseInput($SchemaObject) -} - -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Valid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - IncludeValid = $IncludeValid - IncludeLatent = $IncludeLatent - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefValid = [Ref]$Valid - } - TestObject @TestParams - if ($ValidateOnly) { $Valid } -} diff --git a/_Temp/Test-ObjectGraph3.ps1 b/_Temp/Test-ObjectGraph3.ps1 deleted file mode 100644 index d94cdf1..0000000 --- a/_Temp/Test-ObjectGraph3.ps1 +++ /dev/null @@ -1,583 +0,0 @@ -using module .\..\..\ObjectGraphTools.psm1 - -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Collections -using namespace System.Collections.Generic - -<# -.SYNOPSIS - Tests the properties of an object-graph. - -.DESCRIPTION - Tests an object-graph against a schema object by verifying that the properties of the object-graph - meet the constrains defined in the schema object. - - Statements: - * @RequiredNodes defines the required nodes and the order of the nodes to be tested - * Any child node that isn't listed in the `@Required` condition (even negated, as e.g.: `-Not NodeName`) - is considered optional - * @AllowExtraNodes: when set, optional nodes are required at least once and any additional (undefined) node is - unconditional excepted - - -#> - -[Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] - $SchemaObject, - - [Parameter(ParameterSetName='ValidateOnly')] - [Switch]$ValidateOnly, - - [Parameter(ParameterSetName='ResultList')] - [Alias('All')][Switch]$IncludeAll, - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - $AssertPrefix = '@', - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - -# JsonSchema Properties -# Schema properties: [NewtonSoft.Json.Schema.JsonSchema]::New() | Get-Member -# https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Schema_JsonSchema.htm - - - Enum UniqueType { None; Node; Match } # if a node isn't unique the related option isn't uniquely matched either - Enum CompareType { Scalar; OneOf; AllOf } - - $Script:Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } - - function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } - - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$($SchemaNode.Synopsys) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object - } - - $LimitTests = [Ordered]@{ - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - ExclusiveMinimum = 'The value is greater than' - Minimum = 'The value is greater than or equal to' - } - - $MatchTests = [Ordered]@{ - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' - } - - $Script:Tests = [Ordered]@{ - Title = 'Title' - References = 'Assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Required = 'The node is required' - Unique = 'The node is unique' - } + - $LimitTests + - $MatchTests + - [Ordered]@{ - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - AllowExtraNodes = 'Allow undefined child nodes' - } - - function ResolveReferences($Node) { - if ($Node.Cache.ContainsKey('TestReferences')) { return } - $Stack = [Stack]::new() - while ($true) { - $ParentNode = $Node.ParentNode - if ($ParentNode -and -not $ParentNode.Cache.ContainsKey('TestReferences')) { - $Stack.Push($Node) - $Node = $ParentNode - continue - } - $RefNode = if ($Node.Contains('@References')) { $Node.GetChildNode('@References') } - $Node.Cache['TestReferences'] = [HashTable]::new($Ordinal[[Bool]$RefNode.IsCaseSensitive]) - if ($RefNode) { - foreach ($ChildNode in $RefNode.ChildNodes) { - if (-not $Node.Cache['TestReferences'].ContainsKey($ChildNode.Name)) { - $Node.Cache['TestReferences'][$ChildNode.Name] = $ChildNode - } - } - } - $ParentNode = $Node.ParentNode - if ($ParentNode) { - foreach ($RefName in $ParentNode.Cache['TestReferences'].get_Keys()) { - if (-not $Node.Cache['TestReferences'].ContainsKey($RefName)) { - $Node.Cache['TestReferences'][$RefName] = $ParentNode.Cache['TestReferences'][$RefName] - } - } - } - if ($Stack.Count -eq 0) { break } - $Node = $Stack.Pop() - } - } - - function MatchNode ( - [PSNode]$ObjectNode, - [PSNode]$TestNode, - [Switch]$ValidateOnly, - [Switch]$IncludeAll, - [Switch]$Ordered, - [Nullable[Bool]]$CaseSensitive, - [Switch]$MatchAll, - $MatchedNames - ) { - $ChildNode, $Violates = $null - $Name = $TestNode.Name - - if ($TestNode -is [PSLeafNode]) { - $ParentNode = $TestNode.ParentNode - $References = if ($ParentNode) { - if (-not $ParentNode.Cache.ContainsKey('TestReferences')) { ResolveReferences $ParentNode } - $ParentNode.Cache['TestReferences'] - } else { @{} } - if ($References.Contains($TestNode.Value)) { - $AssertNode = $References[$TestNode.Value] - $AssertNode.Cache['TestReferences'] = $References - } - else { SchemaError "Unknown reference: $($TestNode.Value)" $ObjectNode $TestNode } - } else { $AssertNode = $TestNode } - - $ChildNodes = $ObjectNode.ChildNodes - if ($ObjectNode -is [PSMapNode] -and $TestNode.NodeOrigin -eq 'Map') { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "Node $Name should be in order" - } - } else { $ChildNode = $false } - } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Should contain at least $($TestNodes.Count) nodes" - } - $ChildNode = $ChildNodes[$NodeIndex] - } - - if ($Violates) { - if (-not $ValidateOnly) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $AssertNode - Valid = -not $Violates - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResult') - $Output - } - return - } - else { - if ($ChildNode -is [PSNode]) { - $Violates = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - IncludeAll = $IncludeAll - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Violates - } - TestNode @TestParams - if (-not $Violates) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Violates = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - IncludeAll = $IncludeAll - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Violates - } - TestNode @TestParams - if (-not $Violates) { - $null = $MatchedNames.Add($ChildNode.Name) - if (-not $MatchAll) { break } - } - elseif ($IncludeAll) { $Violates } - } - } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } - } - } - - function TestNode ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$IncludeAll, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity from the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node - ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ANSI]::ParameterColor)Caller (line: $($Caller.ScriptLineNumber))$([ANSI]::ResetColor):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ANSI]::ParameterColor)ObjectNode:$([ANSI]::ResetColor)" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ANSI]::ParameterColor)SchemaNode:$([ANSI]::ResetColor)" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ANSI]::ParameterColor)ValidateOnly:$([ANSI]::ResetColor)" ([Bool]$ValidateOnly) - } - - if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # Allow any node - - $Value = $ObjectNode.Value - $RefInvalidNode.Value = $null - - # Separate the assert nodes from the schema subnodes - $At = [Ordered]@{} # $At{] = $ChildNodes.@ - if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() - foreach ($Node in $SchemaNode.ChildNodes) { - if ($Node.Name.StartsWith($AssertPrefix)) { - $TestName = $Node.Name.SubString($AssertPrefix.Length) - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown assert: '$($Node.Name)'" $ObjectNode $SchemaNode } - $At[$TestName] = $Node - } - else { $TestNodes.Add($Node) } - } - } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - if ($At.Contains('CaseSensitive')) { $CaseSensitive = [Nullable[Bool]]$At['CaseSensitive'] } - -#Region Node validation - - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null - foreach ($TestName in $Tests.Keys) { - if ($TestName -notin $At.Keys) { continue } # Check if ordered test are still required !!! - $AssertNode = $At[$TestName] - $Criteria = $AssertNode.Value - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Title') { $Null } - elseif ($TestName -eq 'References') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' assert requires a collection node" - } - } - elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { - if ($TypeName -in $null, 'Null', 'Void') { - if ($null -eq $Value) { $true; break } - } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } - } - if ($ObjectNode -is $Type -or $Value -is $Type) { $true; break } - } - $Violates = $null -eq $FoundType -xor $TestName -eq 'notType' - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "Invalid case sensitivity value: $Criteria" $ObjectNode $SchemaNode - } - } - elseif ($TestName -eq 'ExclusiveMinimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } - } - elseif ($TestName -eq 'Minimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } - } - elseif ($TestName -eq 'ExclusiveMaximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } - } - elseif ($TestName -eq 'Maximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } - } - - elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - $Match = foreach ($AnyCriteria in $Criteria) { - $IsMatch = if ($TestName.EndsWith('Like', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iLike $AnyCriteria } - else { $Value -Like $AnyCriteria } - } - else { # if ($TestName.EndsWith('Match', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $Value -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $Value -iMatch $AnyCriteria } - else { $Value -Match $AnyCriteria } - } - if ($IsMatch) { $true; break } - } - $Violates = -not $Match -xor $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - } - elseif ($TestName -eq 'Required') { } - elseif ($TestName -eq 'Unique') { - $ParentNode = $ObjectNode.ParentNode - if (-not $ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode - } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) - foreach ($SiblingNode in $ParentNode.ChildNodes) { - if ($ObjectNode.Name -ceq $SiblingNode.Name) { continue } # Self - if ($ObjectComparer.IsEqual($ObjectNode, $SiblingNode)) { - $Violates = $true - break - } - } - } - elseif ($TestName -in 'Ordered', 'RequiredNodes', 'AllowExtraNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' assert requires a collection node" - } - } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } - - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } - - if ($Violates -or $IncludeAll) { - $Condition = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName]) } - else { "$($Tests[$TestName]) $(@($Criteria).foreach{ [PSSerialize]$_ } -Join ', ')" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $IncludeAll) { <# Write-Output #> $Output } - } - } - -#EndRegion Node validation - - if ($Violates) { return } - -#Region Required nodes - - $ChildNodes = $ObjectNode.ChildNodes - $CaseSensitiveNames = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.IsCaseSensitive } - $AssertResults = [HashTable]::new($Ordinal[[Bool]$CaseSensitiveNames]) - - $RequiredNodes = $At['RequiredNodes'] - $RequiredList = if ($RequiredNodes) { [List[Object]]$RequiredNodes.Value } else { [List[Object]]::new() } - foreach ($ChildNode in $ChildNodes) { - if ($ChildNode -is [PSCollectionNode] -and $ChildNode.GetValue('@Required')) { $RequiredNodes.Add($ChildNode.Name) } - } - - foreach ($Requirement in $RequiredList) { - $LogicalFormula = [LogicalFormula]$Requirement - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $null - Operator = $null - Negate = $null - }) - $Term, $Operand, $Accumulator = $null - While ($Stack.Count -gt 0) { - # Accumulator = Accumulator Operand - # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Operator = $Pop.Operator - if ($null -eq $Operator) { $Operand = $Pop.Accumulator } - else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } - $Negate = $Pop.Negate - $Compute = $null -notin $Operand, $Operator, $Accumulator - while ($Compute -or $Enumerator.MoveNext()) { - if ($Compute) { $Compute = $false} - else { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $SchemaNode.GetChildNode($Name) - IncludeAll = $IncludeAll - ValidateOnly = $ValidateOnly - Ordered = $At['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = $false - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - $AssertResults[$Name] = $MatchedNames.Count -gt $MatchCount0 - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term.Value -eq 'Not') { $Negate = -Not $Negate } - elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } - else { SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode } - } - elseif ($Term -is [LogicalFormula]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator, $Operator, $Negate = $null - $Enumerator = $Term.Terms.GetEnumerator() - continue - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - $Negate = $null - if ($Operator -eq 'And') { - $Operator = $null - if ($Accumulator -eq $false -and -not $At['AllowExtraNodes']) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - $Operator = $null - if ($Accumulator -eq $true -and -not $At['AllowExtraNodes']) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Operator = $null - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand = $Null - } - } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Operator" $ObjectNode $SchemaNode - } - } - if ($Accumulator -eq $False) { - $Violates = "Meets the conditions of the nodes $LogicalFormula" - break - } - } - -#EndRegion Required nodes - -#Region Optional nodes - - if (-not $Violates) { - - foreach ($TestNode in $TestNodes) { - if ($MatchedNames.Count -ge $ChildNodes.Count) { break } - if ($AssertResults.Contains($TestNode.Name)) { continue } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $TestNode - IncludeAll = $IncludeAll - ValidateOnly = $ValidateOnly - Ordered = $At['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = -not $At['AllowExtraNodes'] - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - if ($At['AllowExtraNodes'] -and $MatchedNames.Count -eq $MatchCount0) { - $Violates = "When extra nodes are allowed, the node $($TestNode.Name) should be accepted" - break - } - $AssertResults[$TestNode.Name] = $MatchedNames.Count -gt $MatchCount0 - } - - if (-not $At['AllowExtraNodes'] -and $MatchedNames.Count -lt $ChildNodes.Count) { - $Extra = $ChildNodes.Name.where{ -not $MatchedNames.Contains($_) }.foreach{ [PSSerialize]$_ } -Join ', ' - $Violates = "All the child nodes should be accepted, including the nodes: $Extra" - } - } - -#EndRegion Optional nodes - - if ($Violates -or $IncludeAll) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Condition = if ($Violates) { $Violates } else { 'All the child nodes should be accepted'} - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { $RefInvalidNode.Value = $Output } - if (-not $ValidateOnly -or $IncludeAll) { <# Write-Output #> $Output } - } - } - - $SchemaNode = [PSNode]::ParseInput($SchemaObject) -} - -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - IncludeAll = $IncludeAll - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestNode @TestParams - if ($ValidateOnly) { -not $Invalid } - -} - diff --git a/_Temp/Test-ObjectGraph4.ps1 b/_Temp/Test-ObjectGraph4.ps1 deleted file mode 100644 index 8b80245..0000000 --- a/_Temp/Test-ObjectGraph4.ps1 +++ /dev/null @@ -1,606 +0,0 @@ -using module .\..\..\ObjectGraphTools.psm1 - -using namespace System.Management.Automation -using namespace System.Management.Automation.Language -using namespace System.Collections -using namespace System.Collections.Generic - -<# -.SYNOPSIS - Tests the properties of an object-graph. - -.DESCRIPTION - Tests an object-graph against a schema object by verifying that the properties of the object-graph - meet the constrains defined in the schema object. - - Statements: - * @RequiredNodes defines the required nodes and the order of the nodes to be tested - * Any child node that isn't listed in the `@Required` condition (even negated, as e.g.: `-Not NodeName`) - is considered optional - * @AllowExtraNodes: when set, optional nodes are required at least once and any additional (undefined) node is - unconditional excepted - - -#> - -[Alias('Test-Object', 'tso')] -[CmdletBinding(DefaultParameterSetName = 'ResultList', HelpUri='https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Test-ObjectGraph.md')][OutputType([String])] param( - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, ValueFromPipeLine = $True)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, ValueFromPipeLine = $True)] - $InputObject, - - [Parameter(ParameterSetName='ValidateOnly', Mandatory = $true, Position = 0)] - [Parameter(ParameterSetName='ResultList', Mandatory = $true, Position = 0)] - $SchemaObject, - - [Parameter(ParameterSetName='ValidateOnly')] - [Switch]$ValidateOnly, - - [Parameter(ParameterSetName='ResultList')] - [Switch]$Elaborate, - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - $AssertPrefix = '@', - - [Parameter(ParameterSetName='ValidateOnly')] - [Parameter(ParameterSetName='ResultList')] - [Alias('Depth')][int]$MaxDepth = [PSNode]::DefaultMaxDepth -) - -begin { - -# JsonSchema Properties -# Schema properties: [NewtonSoft.Json.Schema.JsonSchema]::New() | Get-Member -# https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Schema_JsonSchema.htm - - - Enum UniqueType { None; Node; Match } # if a node isn't unique the related option isn't uniquely matched either - Enum CompareType { Scalar; OneOf; AllOf } - - $Script:Ordinal = @{$false = [StringComparer]::OrdinalIgnoreCase; $true = [StringComparer]::Ordinal } - - function StopError($Exception, $Id = 'TestNode', $Category = [ErrorCategory]::SyntaxError, $Object) { - if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } - elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } - $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Category, $Object)) - } - - function SchemaError($Message, $ObjectNode, $SchemaNode, $Object = $SchemaObject) { - $Exception = [ArgumentException]"$($SchemaNode.Synopsys) $Message" - $Exception.Data.Add('ObjectNode', $ObjectNode) - $Exception.Data.Add('SchemaNode', $SchemaNode) - StopError -Exception $Exception -Id 'SchemaError' -Category InvalidOperation -Object $Object - } - - $LimitTests = [Ordered]@{ - ExclusiveMaximum = 'The value is less than' - Maximum = 'The value is less than or equal to' - ExclusiveMinimum = 'The value is greater than' - Minimum = 'The value is greater than or equal to' - } - - $MatchTests = [Ordered]@{ - Like = 'The value is like' - Match = 'The value matches' - NotLike = 'The value is not like' - NotMatch = 'The value not matches' - } - - $Script:Tests = [Ordered]@{ - Title = 'Title' - References = 'Assert references' - Type = 'The node or value is of type' - NotType = 'The node or value is not type' - CaseSensitive = 'The (descendant) node are considered case sensitive' - Required = 'The node is required' - Unique = 'The node is unique' - } + - $LimitTests + - $MatchTests + - [Ordered]@{ - Ordered = 'The nodes are in order' - RequiredNodes = 'The node contains the nodes' - AllowExtraNodes = 'Allow undefined child nodes' - } - - $At = @{} - $Tests.Get_Keys().Foreach{ $At[$_] = "$($AssertPrefix)$_" } - - function ResolveReferences($Node) { - if ($Node.Cache.ContainsKey('TestReferences')) { return } - $Stack = [Stack]::new() - while ($true) { - $ParentNode = $Node.ParentNode - if ($ParentNode -and -not $ParentNode.Cache.ContainsKey('TestReferences')) { - $Stack.Push($Node) - $Node = $ParentNode - continue - } - $RefNode = if ($Node.Contains($At.References)) { $Node.GetChildNode($At.References) } - $Node.Cache['TestReferences'] = [HashTable]::new($Ordinal[[Bool]$RefNode.IsCaseSensitive]) - if ($RefNode) { - foreach ($ChildNode in $RefNode.ChildNodes) { - if (-not $Node.Cache['TestReferences'].ContainsKey($ChildNode.Name)) { - $Node.Cache['TestReferences'][$ChildNode.Name] = $ChildNode - } - } - } - $ParentNode = $Node.ParentNode - if ($ParentNode) { - foreach ($RefName in $ParentNode.Cache['TestReferences'].get_Keys()) { - if (-not $Node.Cache['TestReferences'].ContainsKey($RefName)) { - $Node.Cache['TestReferences'][$RefName] = $ParentNode.Cache['TestReferences'][$RefName] - } - } - } - if ($Stack.Count -eq 0) { break } - $Node = $Stack.Pop() - } - } - - function MatchNode ( - [PSNode]$ObjectNode, - [PSNode]$TestNode, - [Switch]$ValidateOnly, - [Switch]$Elaborate, - [Switch]$Ordered, - [Nullable[Bool]]$CaseSensitive, - [Switch]$MatchAll, - $MatchedNames - ) { - $Violates = $null - $Name = $TestNode.Name - - $ChildNodes = $ObjectNode.ChildNodes - if ($ChildNodes.Count -eq 0) { return } - - if ($TestNode -is [PSLeafNode]) { - $ParentNode = $TestNode.ParentNode - $References = if ($ParentNode) { - if (-not $ParentNode.Cache.ContainsKey('TestReferences')) { ResolveReferences $ParentNode } - $ParentNode.Cache['TestReferences'] - } else { @{} } - if ($References.Contains($TestNode.Value)) { - $AssertNode = $References[$TestNode.Value] - $AssertNode.Cache['TestReferences'] = $References - } - else { SchemaError "Unknown reference: $($TestNode.Value)" $ObjectNode $TestNode } - } else { $AssertNode = $TestNode } - - if ($ObjectNode -is [PSMapNode] -and $TestNode.NodeOrigin -eq 'Map') { - if ($ObjectNode.Contains($Name)) { - $ChildNode = $ObjectNode.GetChildNode($Name) - if ($Ordered -and $ChildNodes.IndexOf($ChildNode) -ne $TestNodes.IndexOf($TestNode)) { - $Violates = "Node $Name should be in order" - } - } else { $ChildNode = $false } - } - elseif ($ChildNodes.Count -eq 1) { $ChildNode = $ChildNodes[0] } - elseif ($Ordered) { - $NodeIndex = $TestNodes.IndexOf($TestNode) - if ($NodeIndex -ge $ChildNodes.Count) { - $Violates = "Should contain at least $($TestNodes.Count) nodes" - } - $ChildNode = $ChildNodes[$NodeIndex] - } - else { $ChildNode = $null } - - if ($Violates) { - if (-not $ValidateOnly) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $AssertNode - Valid = -not $Violates - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResult') - $Output - } - return - } - else { - if ($ChildNode -is [PSNode]) { - $Violates = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Violates - } - TestNode @TestParams - if (-not $Violates) { $null = $MatchedNames.Add($ChildNode.Name) } - } - elseif ($null -eq $ChildNode) { - foreach ($ChildNode in $ChildNodes) { - if ($MatchedNames.Contains($ChildNode.Name)) { continue } - $Violates = $Null - $TestParams = @{ - ObjectNode = $ChildNode - SchemaNode = $AssertNode - Elaborate = $Elaborate - CaseSensitive = $CaseSensitive - ValidateOnly = $true - RefInvalidNode = [Ref]$Violates - } - TestNode @TestParams - if (-not $Violates) { - $null = $MatchedNames.Add($ChildNode.Name) - if (-not $MatchAll) { break } - } - elseif ($Elaborate) { $Violates } - } - } - elseif ($ChildNode -eq $false) { $AssertResults[$Name] = $false } - else { throw "Unexpected return reference: $ChildNode" } - } - } - - function TestNode ( - [PSNode]$ObjectNode, - [PSNode]$SchemaNode, - [Switch]$Elaborate, # if set, include the failed test results in the output - [Nullable[Bool]]$CaseSensitive, # inherited the CaseSensitivity from the parent node if not defined - [Switch]$ValidateOnly, # if set, stop at the first invalid node - $RefInvalidNode # references the first invalid node - ) { - $CallStack = Get-PSCallStack - # if ($CallStack.Count -gt 20) { Throw 'Call stack failsafe' } - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - $Caller = $CallStack[1] - Write-Host "$([ANSI]::ParameterColor)Caller (line: $($Caller.ScriptLineNumber))$([ANSI]::ResetColor):" $Caller.InvocationInfo.Line.Trim() - Write-Host "$([ANSI]::ParameterColor)ObjectNode:$([ANSI]::ResetColor)" $ObjectNode.Path "$ObjectNode" - Write-Host "$([ANSI]::ParameterColor)SchemaNode:$([ANSI]::ResetColor)" $SchemaNode.Path "$SchemaNode" - Write-Host "$([ANSI]::ParameterColor)ValidateOnly:$([ANSI]::ResetColor)" ([Bool]$ValidateOnly) - } - - if ($SchemaNode -is [PSListNode] -and $SchemaNode.Count -eq 0) { return } # Allow any node - - $Value = $ObjectNode.Value - $RefInvalidNode.Value = $null - - # Separate the assert nodes from the schema subnodes - $AssertNodes = [Ordered]@{} # $AssertNodes{] = $ChildNodes.@ - if ($SchemaNode -is [PSMapNode]) { - $TestNodes = [List[PSNode]]::new() - foreach ($Node in $SchemaNode.ChildNodes) { - if ($Node.Name.StartsWith($AssertPrefix)) { - $TestName = $Node.Name.SubString($AssertPrefix.Length) - if ($TestName -notin $Tests.Keys) { SchemaError "Unknown assert: '$($Node.Name)'" $ObjectNode $SchemaNode } - $AssertNodes[$TestName] = $Node - } - else { $TestNodes.Add($Node) } - } - } - elseif ($SchemaNode -is [PSListNode]) { $TestNodes = $SchemaNode.ChildNodes } - else { $TestNodes = @() } - - if ($AssertNodes.Contains('CaseSensitive')) { $CaseSensitive = [Nullable[Bool]]$AssertNodes['CaseSensitive'] } - -#Region Node validation - - $RefInvalidNode.Value = $false - $MatchedNames = [HashSet[Object]]::new() - $AssertResults = $Null - foreach ($TestName in $AssertNodes.get_Keys()) { - $AssertNode = $AssertNodes[$TestName] - $Criteria = $AssertNode.Value - $Violates = $null # is either a boolean ($true if invalid) or a string with what was expected - if ($TestName -eq 'Title') { $Null } - elseif ($TestName -eq 'References') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' assert requires a collection node" - } - } - elseif ($TestName -in 'Type', 'notType') { - $FoundType = foreach ($TypeName in $Criteria) { - if ($TypeName -in $null, 'Null', 'Void') { - if ($null -eq $Value) { $true; break } - } - elseif ($TypeName -is [Type]) { $Type = $TypeName } else { - $Type = $TypeName -as [Type] - if (-not $Type) { - SchemaError "Unknown type: $TypeName" $ObjectNode $SchemaNode - } - } - if ($ObjectNode -is $Type -or $Value -is $Type) { $true; break } - } - $Violates = $null -eq $FoundType -xor $TestName -eq 'notType' - } - elseif ($TestName -eq 'CaseSensitive') { - if ($null -ne $Criteria -and $Criteria -isnot [Bool]) { - SchemaError "Invalid case sensitivity value: $Criteria" $ObjectNode $SchemaNode - } - } - elseif ($TestName -eq 'ExclusiveMinimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cge $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ige $Value } - else { $Criteria -ge $Value } - } - elseif ($TestName -eq 'Minimum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cgt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -igt $Value } - else { $Criteria -gt $Value } - } - elseif ($TestName -eq 'ExclusiveMaximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -cle $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ile $Value } - else { $Criteria -le $Value } - } - elseif ($TestName -eq 'Maximum') { - $Violates = - if ($CaseSensitive -eq $true) { $Criteria -clt $Value } - elseif ($CaseSensitive -eq $false) { $Criteria -ilt $Value } - else { $Criteria -lt $Value } - } - - elseif ($TestName -in 'Like', 'NotLike', 'Match', 'NotMatch') { - $Negate = $TestName.StartsWith('Not', 'OrdinalIgnoreCase') - foreach ($x in $Value) { - $Match = $false - foreach ($AnyCriteria in $Criteria) { - $Match = if ($TestName.EndsWith('Like', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $x -cLike $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $x -iLike $AnyCriteria } - else { $x -Like $AnyCriteria } - } - else { # if ($TestName.EndsWith('Match', 'OrdinalIgnoreCase')) { - if ($true -eq $CaseSensitive) { $x -cMatch $AnyCriteria } - elseif ($false -eq $CaseSensitive) { $x -iMatch $AnyCriteria } - else { $x -Match $AnyCriteria } - } - if ($Match) { break } - } - $Violates = -not $Match -xor $Negate - if ($Violates) { break } - } - } - elseif ($TestName -eq 'Required') { } - elseif ($TestName -eq 'Unique') { - $ParentNode = $ObjectNode.ParentNode - if (-not $ParentNode) { - SchemaError "The unique assert can't be used on a root node" $ObjectNode $SchemaNode - } - $ObjectComparer = [ObjectComparer]::new([ObjectComparison][Int][Bool]$CaseSensitive) - foreach ($SiblingNode in $ParentNode.ChildNodes) { - if ($ObjectNode.Name -ceq $SiblingNode.Name) { continue } # Self - if ($ObjectComparer.IsEqual($ObjectNode, $SiblingNode)) { - $Violates = $true - break - } - } - } - elseif ($TestName -eq 'AllowExtraNodes') {} - elseif ($TestName -in 'Ordered', 'RequiredNodes') { - if ($ObjectNode -isnot [PSCollectionNode]) { - $Violates = "The '$($AssertNode.Name)' assert requires a collection node" - } - } - else { SchemaError "Unknown assert node: $TestName" $ObjectNode $SchemaNode } - - if ($DebugPreference -in 'Stop', 'Continue', 'Inquire') { - if (-not $Violates) { Write-Host -ForegroundColor Green "Valid: $TestName $Criteria" } - else { Write-Host -ForegroundColor Red "Invalid: $TestName $Criteria" } - } - - if ($Violates -or $Elaborate) { - $Condition = - if ($Violates -is [String]) { $Violates } - elseif ($Criteria -eq $true) { $($Tests[$TestName]) } - else { "$($Tests[$TestName]) $(@($Criteria).foreach{ [PSSerialize]$_ } -Join ', ')" } - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Condition = $Condition - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { - $RefInvalidNode.Value = $Output - if ($ValidateOnly) { return } - } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } - } - } - -#EndRegion Node validation - - if ($Violates) { return } - -#Region Required nodes - - $ChildNodes = $ObjectNode.ChildNodes - - if ($TestNodes.Count -and -not $AssertNodes.Contains('Type')) { - if ($SchemaNode -is [PSListNode] -and $ObjectNode -isnot [PSListNode]) { - $Violates = 'Expected a list node' - } - if ($SchemaNode -is [PSMapNode] -and $ObjectNode -isnot [PSMapNode]) { - $Violates = 'Expected a map node' - } - } - - if (-Not $Violates) { - $RequiredNodes = $AssertNodes['RequiredNodes'] - $CaseSensitiveNames = if ($ObjectNode -is [PSMapNode]) { $ObjectNode.IsCaseSensitive } - $AssertResults = [HashTable]::new($Ordinal[[Bool]$CaseSensitiveNames]) - - if ($RequiredNodes) { $RequiredList = [List[Object]]$RequiredNodes.Value } else { $RequiredList = [List[Object]]::new() } - foreach ($TestNode in $TestNodes) { - if ($TestNode -is [PSMapNode] -and $TestNode.GetValue($At.Required)) { $RequiredList.Add($TestNode.Name) } - } - - foreach ($Requirement in $RequiredList) { - $LogicalFormula = [LogicalFormula]$Requirement - $Enumerator = $LogicalFormula.Terms.GetEnumerator() - $Stack = [Stack]::new() - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $null - Operator = $null - Negate = $null - }) - $Term, $Operand, $Accumulator = $null - While ($Stack.Count -gt 0) { - # Accumulator = Accumulator Operand - # if ($Stack.Count -gt 20) { Throw 'Formula stack failsafe'} - $Pop = $Stack.Pop() - $Enumerator = $Pop.Enumerator - $Operator = $Pop.Operator - if ($null -eq $Operator) { $Operand = $Pop.Accumulator } - else { $Operand, $Accumulator = $Accumulator, $Pop.Accumulator } - $Negate = $Pop.Negate - $Compute = $null -notin $Operand, $Operator, $Accumulator - while ($Compute -or $Enumerator.MoveNext()) { - if ($Compute) { $Compute = $false} - else { - $Term = $Enumerator.Current - if ($Term -is [LogicalVariable]) { - $Name = $Term.Value - if (-not $AssertResults.ContainsKey($Name)) { - if (-not $SchemaNode.Contains($Name)) { - SchemaError "Unknown test node: $Term" $ObjectNode $SchemaNode - } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $SchemaNode.GetChildNode($Name) - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = $false - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - $AssertResults[$Name] = $MatchedNames.Count -gt $MatchCount0 - } - $Operand = $AssertResults[$Name] - } - elseif ($Term -is [LogicalOperator]) { - if ($Term.Value -eq 'Not') { $Negate = -Not $Negate } - elseif ($null -eq $Operator -and $null -ne $Accumulator) { $Operator = $Term.Value } - else { SchemaError "Unexpected operator: $Term" $ObjectNode $SchemaNode } - } - elseif ($Term -is [LogicalFormula]) { - $Stack.Push(@{ - Enumerator = $Enumerator - Accumulator = $Accumulator - Operator = $Operator - Negate = $Negate - }) - $Accumulator, $Operator, $Negate = $null - $Enumerator = $Term.Terms.GetEnumerator() - continue - } - else { SchemaError "Unknown logical operator term: $Term" $ObjectNode $SchemaNode } - } - if ($null -ne $Operand) { - if ($null -eq $Accumulator -xor $null -eq $Operator) { - if ($Accumulator) { SchemaError "Missing operator before: $Term" $ObjectNode $SchemaNode } - else { SchemaError "Missing variable before: $Operator $Term" $ObjectNode $SchemaNode } - } - $Operand = $Operand -Xor $Negate - $Negate = $null - if ($Operator -eq 'And') { - $Operator = $null - if ($Accumulator -eq $false -and -not $AssertNodes['AllowExtraNodes']) { break } - $Accumulator = $Accumulator -and $Operand - } - elseif ($Operator -eq 'Or') { - $Operator = $null - if ($Accumulator -eq $true -and -not $AssertNodes['AllowExtraNodes']) { break } - $Accumulator = $Accumulator -Or $Operand - } - elseif ($Operator -eq 'Xor') { - $Operator = $null - $Accumulator = $Accumulator -xor $Operand - } - else { $Accumulator = $Operand } - $Operand = $Null - } - } - if ($null -ne $Operator -or $null -ne $Negate) { - SchemaError "Missing variable after $Operator" $ObjectNode $SchemaNode - } - } - if ($Accumulator -eq $False) { - $Violates = "Meets the conditions of the nodes $LogicalFormula" - break - } - } - } - -#EndRegion Required nodes - -#Region Optional nodes - - if (-not $Violates) { - - foreach ($TestNode in $TestNodes) { - if ($MatchedNames.Count -ge $ChildNodes.Count) { break } - if ($AssertResults.Contains($TestNode.Name)) { continue } - $MatchCount0 = $MatchedNames.Count - $MatchParams = @{ - ObjectNode = $ObjectNode - TestNode = $TestNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - Ordered = $AssertNodes['Ordered'] - CaseSensitive = $CaseSensitive - MatchAll = -not $AssertNodes['AllowExtraNodes'] - MatchedNames = $MatchedNames - } - MatchNode @MatchParams - if ($AssertNodes['AllowExtraNodes'] -and $MatchedNames.Count -eq $MatchCount0) { - $Violates = "When extra nodes are allowed, the node $($TestNode.Name) should be accepted" - break - } - $AssertResults[$TestNode.Name] = $MatchedNames.Count -gt $MatchCount0 - } - - if (-not $AssertNodes['AllowExtraNodes'] -and $MatchedNames.Count -lt $ChildNodes.Count) { - $Extra = $ChildNodes.Name.where{ -not $MatchedNames.Contains($_) }.foreach{ [PSSerialize]$_ } -Join ', ' - $Violates = "All the child nodes should be accepted, including the nodes: $Extra" - } - } - -#EndRegion Optional nodes - - if ($Violates -or $Elaborate) { - $Output = [PSCustomObject]@{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Valid = -not $Violates - Condition = if ($Violates) { $Violates } else { 'All the child nodes should be accepted'} - } - $Output.PSTypeNames.Insert(0, 'TestResult') - if ($Violates) { $RefInvalidNode.Value = $Output } - if (-not $ValidateOnly -or $Elaborate) { <# Write-Output #> $Output } - } - } - - $SchemaNode = [PSNode]::ParseInput($SchemaObject) -} - -process { - $ObjectNode = [PSNode]::ParseInput($InputObject, $MaxDepth) - $Invalid = $Null - $TestParams = @{ - ObjectNode = $ObjectNode - SchemaNode = $SchemaNode - Elaborate = $Elaborate - ValidateOnly = $ValidateOnly - RefInvalidNode = [Ref]$Invalid - } - TestNode @TestParams - if ($ValidateOnly) { -not $Invalid } - -} - diff --git a/_Temp/Test-Ref.ps1 b/_Temp/Test-Ref.ps1 deleted file mode 100644 index 7bda1ba..0000000 --- a/_Temp/Test-Ref.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$Get = $Null - -function Test ($Out) { - $Out.PSTypeNames - if ($Out) { $out.Value = 4 } -} - -$Param = @{ Out = [ref]$Get } -Test @Param -Write-Host 'Get' $Get \ No newline at end of file diff --git a/_Temp/Test-SchemaError.ps1 b/_Temp/Test-SchemaError.ps1 deleted file mode 100644 index e8c4a5c..0000000 --- a/_Temp/Test-SchemaError.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -Using namespace System.Collections -Using namespace System.Collections.Generic - -class PSSchemaError: Exception { - PSSchemaError([IDictionary]$Data, [string]$Message): base ($Message) { - foreach ($key in $Data.Keys) { $this.Data.Add($key, $Data[$key]) } - } -} - -function Test { - $e = [PSSchemaError]::new(@{ Key3 = 'Value3' }, "Test") - throw $e -} - -Try { Test } Catch { $_.Exception.Data } diff --git a/_Temp/Test-Switch.ps1 b/_Temp/Test-Switch.ps1 deleted file mode 100644 index e82eacb..0000000 --- a/_Temp/Test-Switch.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$n = $Null - -$a = 1..5 -$r = :myLabel switch ($a) { - { $true } { $n = 4 } - 2 { Write-Host 'Two' $_ $n } - 3 { 'test'; break mylabel } -} -$r \ No newline at end of file diff --git a/_Temp/Test.ps1 b/_Temp/Test.ps1 deleted file mode 100644 index 12e8f60..0000000 --- a/_Temp/Test.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$f = { param ($a, $b, $c) Write-Host $a $b $c; if ($b.Value) { $b.Value = 42 } } - - -#& $f 1 2 3 - -$a = 1 -$b = @{ Value = 2 } -$c = 3 -& $f $a $b $c -Write-Host $a $b $c - -Function Test-Ref { - param ([ref]$a, [ref]$b, [ref]$c) - $a.Value = 1 - $b.Value = 2 - $c.Value = 3 -} \ No newline at end of file