Source/Public/Copy-ObjectGraph.ps1

<#
.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"
#>

function Copy-ObjectGraph {
    [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()]$ListAs,

        [ValidateNotNull()]$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))
        }

        function NewNode($Object) {
            if ($Null -eq $Object) { return }
            elseif ($Object -is [String]) {
                $TypeName = Switch ($Object) {
                    'ordered'                                     { 'System.Collections.Specialized.OrderedDictionary' }
                    'System.Management.Automation.PSCustomObject' { 'PSCustomObject' }
                    Default                                       { $Object }
                }
                $Type = $TypeName -as [Type]
                if (-not $Type) { StopError "Unknown type: [$Object]" }
                if ('System.Object[]', 'System.Array' -eq $Type.FullName) { $Object = @() }
                else { $Object = New-Object -Type $TypeName }
            }
            elseif ($Type -is [Type]) {
                if ('System.Object[]', 'System.Array' -eq $Type) { $Object = @() }
                elseif ('System.Management.Automation.PSCustomObject' -eq $Type) { $Object = [PSCustomObject]@{} }
                else { $Object = New-Object -Type $Type.FullName }
            }
            [PSNode]::ParseInput($Object)
        }

        $ListNode = NewNode $ListAs
        $MapNode  = NewNode $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 = $Node.ChildNodes.foreach{ CopyObject $_ -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]@{} }
                $Node.ChildNodes.foreach{ $Dictionary[[Object]$_.Name] = CopyObject $_ -ListType $ListType -MapType $MapType }
                if ($IsDirectory) { $Dictionary } else { [PSCustomObject]$Dictionary }
            }
        }
    }
    process {
        $PSNode = [PSNode]::ParseInput($InputObject, $MaxDepth)
        CopyObject $PSNode -ListType $ListType -MapType $MapType -ExcludeLeafs:$ExcludeLeafs
    }
}