src/graph.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'Export-GraphData')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Scope = 'Function', Target = 'New-Graph')]
Param()

function Export-GraphData {
    <#
    .SYNOPSIS
    Export graph data to an XML file or a JSON file.
    .PARAMETER Path
    Path to file intended for data export
    .PARAMETER Force
    Overwrite file at destination path, if one exists
    .EXAMPLE
    $Graph | Export-GraphData
 
    # Export graph data to ./graph.csv
    .EXAMPLE
    $Graph | Export-GraphData -Mermaid -PassThru | Write-Color -Cyan
 
    # Write mermaid format graph data to terminal
    .EXAMPLE
    $Graph | Export-GraphData -JSON -Compress
 
    # Export compressed JSON data (compress also works with XML format)
    .EXAMPLE
    $Graph | Export-GraphData -Format 'XML'
 
    # Supports passing format as string paramter
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [Prelude.Graph] $Graph,
        [ValidateScript( { Test-Path $_ })]
        [String] $Path = (Get-Location),
        [String] $Name = 'graph',
        [Switch] $CSV,
        [Switch] $JSON,
        [Switch] $XML,
        [Switch] $Mermaid,
        [ValidateSet('CSV', 'JSON', 'XML', 'Mermaid')]
        [String] $Format,
        [Switch] $PassThru,
        [Switch] $Compress,
        [Switch] $Force
    )
    $Format = if ($Format.Length -gt 0) {
        $Format
    } else {
        Find-FirstTrueVariable 'CSV', 'JSON', 'XML', 'Mermaid'
    }
    switch ($Format) {
        'CSV' {
            $Name = "${Name}.csv"
            $Result = "SourceId,SourceLabel,TargetId,TargetLabel,Weight,IsDirected`n"
            foreach ($Edge in $Graph.Edges) {
                $Source = $Edge.Source
                $Target = $Edge.Target
                $Result += "$($Source.Id),$($Source.Label),$($Target.Id),$($Target.Label),$($Edge.Weight),$($Edge.IsDirected)`n"
            }
        }
        'JSON' {
            function Format-Node {
                Param(
                    [Parameter(ValueFromPipeline = $True)]
                    [Node] $Node
                )
                Process {
                    $Node | Select-Object 'Id', 'Label'
                }
            }
            $Name = "${Name}.json"
            $Nodes = $Graph.Nodes | Format-Node
            $Edges = $Graph.Edges
            $Result = @{
                Nodes = $Nodes
                Edges = $Edges | ForEach-Object {
                    @{
                        Source = $_.Source | Format-Node
                        Target = $_.Target | Format-Node
                        Weight = $_.Weight
                        IsDirected = $_.IsDirected
                    }
                }
            } | ConvertTo-Json -Depth 3 -Compress:$Compress
        }
        'XML' {
            $Name = "${Name}.xml"
            $Break = if ($Compress) { '' } else { "`n" }
            $Tab = if ($Compress) { '' } else { ' ' }
            $Header = "<?xml version=`"1.0`" encoding=`"UTF-8`"?>${Break}"
            $Result = "${Header}<Graph>${Break}${Tab}<Edges>${Break}"
            $Edges = foreach ($Edge in $Graph.Edges) {
                $EdgeOpen = "<Edge id=`"$($Edge.Id)`" weight=`"$($Edge.Weight)`" directed=`"$($Edge.IsDirected.ToString().ToLower())`">${Break}"
                $Source = "${Tab}${Tab}${Tab}<Node type=`"source`" id=`"$($Edge.Source.Id)`" label=`"$($Edge.Source.Label)`"/>"
                $Target = "${Tab}${Tab}${Tab}<Node type=`"target`" id=`"$($Edge.Target.Id)`" label=`"$($Edge.Target.Label)`"/>"
                "${Tab}${Tab}${EdgeOpen}${Source}${Break}${Target}${Break}${Tab}${Tab}</Edge>"
            }
            $Result += ($Edges -join $Break)
            $Result += "${Break}${Tab}</Edges>${Break}</Graph>"
        }
        'Mermaid' {
            $Name = "${Name}.mmd"
            $Result = "graph TD`n"
            foreach ($Edge in $Graph.Edges) {
                $Source = $Edge.Source
                $Target = $Edge.Target
                $Weight = $Edge.Weight
                $Arrow = if ($Edge.IsDirected) { "-- $Weight -->" } else { "-- $Weight ---" }
                $Result += "`t$($Source.Id)[$($Source.Label)] ${Arrow} $($Target.Id)[$($Target.Label)]`n"
            }
        }
    }
    if ($PassThru) {
        $Result | Write-Verbose
        $Result
    } else {
        $Result | Out-File -FilePath (Join-Path $Path $Name)
    }
}
function Import-GraphData {
    <#
    .SYNOPSIS
    Import graph data from an XML file or a JSON file.
    .PARAMETER Path
    Path to file intended for data import
    .EXAMPLE
    $G = Import-GraphData 'path/to/file.xml'
    #>

    [CmdletBinding()]
    [OutputType([Prelude.Graph])]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $True)]
        [ValidateScript( { Test-Path $_ })]
        [String] $FilePath
    )
    $Extension = [System.IO.Path]::GetExtension($FilePath).Substring(1).ToUpper()
    function Get-Node {
        Param(
            [Graph] $Graph,
            [Node] $Node
        )
        $ExistingNode = $Graph.GetNode($Node.Id)
        if ($ExistingNode) { $ExistingNode } else { $Node }
    }
    $Graph = [Graph]::New()
    switch ($Extension) {
        'CSV' {
            $Data = Import-Csv -Path $FilePath
            foreach ($Item in $Data) {
                $Source = [Node]::New($Item.SourceId, $Item.SourceLabel)
                $Target = [Node]::New($Item.TargetId, $Item.TargetLabel)
                $From = Get-Node -Graph $Graph -Node $Source
                $To = Get-Node -Graph $Graph -Node $Target
                $IsDirected = if ($Item.IsDirected -eq 'True') { $True } else { $False }
                $Edge = New-Edge -From $From -To $To -Weight $Item.Weight -Directed:$IsDirected
                $Graph.Add($From, $To).Add($Edge) | Out-Null
            }
        }
        'JSON' {
            $Data = Get-Content -Path $FilePath | ConvertFrom-Json
            foreach ($Item in $Data.Edges) {
                $Source = [Node]::New($Item.Source.Id, $Item.Source.Label)
                $Target = [Node]::New($Item.Target.Id, $Item.Target.Label)
                $From = Get-Node -Graph $Graph -Node $Source
                $To = Get-Node -Graph $Graph -Node $Target
                $IsDirected = $Item.IsDirected
                $Edge = New-Edge -From $From -To $To -Weight $Item.weight -Directed:$IsDirected
                $Graph.Add($From, $To).Add($Edge) | Out-Null
            }
        }
        'MMD' {
            $IsNotSquareBracket = { Param($X) $X -ne '[' }
            $_, $Lines = (Get-Content -Path $FilePath) -split "`n" | Invoke-Method 'Trim' | Deny-Empty
            $Data = $Lines | Invoke-Operator split '\s+' | Invoke-Chunk -Size 5
            foreach ($Item in $Data) {
                $SourceLabel = if (($Item[0] -match '\[.*\]')) { $Matches[0] } else { 'source' }
                $TargetLabel = if (($Item[4] -match '\[.*\]')) { $Matches[0] } else { 'target' }
                $Source = [Node]::New(($Item[0] | Invoke-TakeWhile $IsNotSquareBracket), $SourceLabel)
                $Target = [Node]::New(($Item[4] | Invoke-TakeWhile $IsNotSquareBracket), $TargetLabel)
                $From = Get-Node -Graph $Graph -Node $Source
                $To = Get-Node -Graph $Graph -Node $Target
                $IsDirected = $Item[3] -eq '-->'
                $Edge = New-Edge -From $From -To $To -Weight $Item[2] -Directed:$IsDirected
                $Graph.Add($From, $To).Add($Edge) | Out-Null
            }
        }
        'XML' {
            [Xml]$Data = Get-Content -Path $FilePath
            foreach ($Item in $Data.Graph.Edges.Edge) {
                $Source = [Node]::New($Item.Node[0].id, $Item.Node[0].label)
                $Target = [Node]::New($Item.Node[1].id, $Item.Node[1].label)
                $From = Get-Node -Graph $Graph -Node $Source
                $To = Get-Node -Graph $Graph -Node $Target
                $IsDirected = if ($Item.directed -eq 'true') { $True } else { $False }
                $Edge = New-Edge -From $From -To $To -Weight $Item.weight -Directed:$IsDirected
                $Graph.Add($From, $To).Add($Edge) | Out-Null
            }
        }
    }
    $Graph
}
function New-Edge {
    <#
    .SYNOPSIS
    Helper cmdlet for creating graph edge objects
    .PARAMETER From
    One node of edge. If edge is directed, this node will be the "source" node.
    .PARAMETER To
    One node of edge. If edge is directed, this node will be the "detination" node.
    .PARAMETER Weight
    Edge weight. A graph can be regarded as "un-weighted" when all edges have the same weight.
    .PARAMETER Directed
    Switch to designate an edge as directed.
    .EXAMPLE
    $A = [Node]'a'
    $B = [Node]'b'
    $AB = New-Edge $A $B
    .EXAMPLE
    $AB = New-Edge 'a' 'b'
    #>

    [CmdletBinding()]
    [Alias('edge')]
    [OutputType([Prelude.Edge])]
    Param(
        [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)]
        [Node] $From,
        [Parameter(Mandatory = $True, Position = 1)]
        [Node] $To,
        [Int] $Weight = 1,
        [Switch] $Directed
    )
    if ($Directed) {
        New-Object 'Prelude.DirectedEdge' @($From, $To, $Weight)
    } else {
        New-Object 'Prelude.Edge' @($From, $To, $Weight)
    }
}
function New-Graph {
    <#
    .SYNOPSIS
    Helper cmdlet for creating graph edge objects
    .PARAMETER Nodes
    Array of graph nodes
    .PARAMETER Edges
    Array of graph edges
    .EXAMPLE
    $G = New-Graph $Nodes $Edges
    .EXAMPLE
    $G = $Edges | New-Graph
    .EXAMPLE
    $K4 = New-Graph -Complete -N 4
    #>

    [CmdletBinding(DefaultParameterSetName = 'custom')]
    [OutputType([Graph])]
    Param(
        [Parameter(ParameterSetName = 'custom', Position = 0)]
        [Alias('V')]
        [Node[]] $Nodes,
        [Parameter(ParameterSetName = 'custom', Position = 1, ValueFromPipeline = $True)]
        [Alias('E')]
        [Edge[]] $Edges,
        [Parameter(ParameterSetName = 'custom')]
        [Switch] $Custom,
        [Parameter(ParameterSetName = 'complete')]
        [Switch] $Complete,
        [Parameter(ParameterSetName = 'smallworld')]
        [Alias('SWN')]
        [Switch] $SmallWorld,
        [Parameter(ParameterSetName = 'bipartite')]
        [Switch] $Bipartite,
        [Parameter(ParameterSetName = 'bipartite')]
        [Int] $Left,
        [Parameter(ParameterSetName = 'bipartite')]
        [Int] $Right,
        [Parameter(ParameterSetName = 'complete', Mandatory = $True)]
        [Parameter(ParameterSetName = 'smallworld', Mandatory = $True)]
        [Alias('N')]
        [Int] $NodeCount,
        [Parameter(ParameterSetName = 'smallworld', Mandatory = $True)]
        [Alias('K')]
        [Double] $MeanDegree
    )
    Begin {
        $GraphType = Find-FirstTrueVariable 'Custom', 'Complete', 'SmallWorld', 'Bipartite'
        function Invoke-NewGraph {
            Param(
                [Edge[]] $Edges
            )
            switch ($GraphType) {
                'Complete' {
                    "==> Creating complete graph with ${NodeCount} nodes" | Write-Verbose
                    [Graph]::Complete($NodeCount)
                    break
                }
                'SmallWorld' {
                    '==> Creating small world graph' | Write-Verbose
                    break
                }
                'Bipartite' {
                    '==> Creating Bipartite graph' | Write-Verbose
                    [Graph]::Bipartite($Left, $Right)
                    break
                }
                Default {
                    if ($Nodes.Count -gt 0) {
                        "==> Creating custom graph with $($Nodes.Count) nodes and $($Edges.Count) edges" | Write-Verbose
                        [Graph]::New($Nodes, $Edges)
                    } elseif ($Edges.Count -gt 0) {
                        "==> Creating custom graph from $($Edges.Count) edges" | Write-Verbose
                        [Graph]::New($Edges)
                    }
                }
            }
        }
        if ($Edges.Count -gt 0 -or $GraphType -ne 'Custom') {
            Invoke-NewGraph -Edges $Edges
        }
    }
    End {
        if ($Input.Count -gt 0 -and $GraphType -eq 'Custom') {
            Invoke-NewGraph -Edges $Input
        }
    }
}