Public/Get-HDTConsoleDependencyChoice.ps1

function Get-HDTConsoleDependencyChoice {
    <#
        .SYNOPSIS
            What an application's "Depends on" can be set to, and what it cannot.
 
        .DESCRIPTION
            A DEPENDENCY IS AN APPLICATION ID, AND TYPING ONE IS THE PROBLEM.
            Every way of getting it wrong fails late and badly: a misspelled id
            is not caught until a deployment runs and Resolve-HDTApplicationOrder
            refuses the whole plan - not just that application - on the machine
            in front of somebody. A pair that ends up depending on each other
            fails the same way, and nothing on the share looks wrong until then.
 
            SO IT IS PICKED. The list is the applications on this share, which
            makes a missing id impossible to produce, and the ticks are what the
            document already says.
 
            A CHOICE THAT WOULD CLOSE A LOOP IS OFFERED AND REFUSED, not hidden.
            An item that vanishes is a window somebody argues with; an item that
            is there, greyed, naming the loop it would make, is a window that
            answers "why can I not tick this". The chain is followed, not just
            the first step: A depends on B and B on C, so C depending on A closes
            a loop three long, and that is exactly what the resolver refuses.
 
            AN APPLICATION THAT WILL NOT READ IS NOT OFFERED. A row whose app.yaml
            failed to parse carries its folder name as an id and nothing else, so
            depending on it would write an id that may not be what the document
            says.
 
            THIS IS THE DECISION, NOT THE DIALOG. What is on the list, what is
            ticked and what cannot be is settled here, against no window at all.
 
        .PARAMETER Application
            The share's application rows, as Get-HDTConsoleWorkspace returns
            them: Id, Name, Dependency and Status.
 
        .PARAMETER Id
            The application being edited. It is never offered to itself.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject, one per offer:
 
              Id the id a document would hold
              Display the name a technician reads
              Selected the document already depends on it
              Blocked choosing it would close a loop
              Choosable the same answer the other way up, because a tick box
                        binds IsEnabled and XAML cannot negate a binding
              Reason which loop, when it would
 
        .EXAMPLE
            Get-HDTConsoleDependencyChoice -Application $share.Application -Id 'Contoso-Suite'
 
        .LINK
            Set-HDTApplication
 
        .LINK
            Resolve-HDTApplicationOrder
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        [object[]] $Application,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Id
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $readable = @($Application | Where-Object { [string] $_.Status -ne 'Error' })

    $dependencyOf = @{}
    $nameOf = @{}

    foreach ($current in @($readable)) {
        $key = ([string] $current.Id).ToLowerInvariant()

        $dependencyOf[$key] = @(@($current.Dependency) | ForEach-Object { ([string] $_).ToLowerInvariant() })
        $nameOf[$key] = [string] $current.Name
    }

    $me = $Id.ToLowerInvariant()

    $mine = @()
    if ($dependencyOf.ContainsKey($me)) { $mine = @($dependencyOf[$me]) }

    # DOES THIS CANDIDATE ALREADY DEPEND ON ME, at any depth? If it does, my
    # depending on it closes a loop - which is the one thing
    # Resolve-HDTApplicationOrder cannot order.
    #
    # $seen IS WHAT MAKES IT TERMINATE. A share that already holds a cycle -
    # hand-edited, or written before this window existed - would otherwise walk
    # it forever, and a console that hangs on a bad document is worse than one
    # that shows it.
    $reaches = {
        param([string] $From, [string] $Wanted, $Seen)

        if ($Seen.Contains($From)) { return $false }
        [void] $Seen.Add($From)

        if (-not $dependencyOf.ContainsKey($From)) { return $false }

        foreach ($step in @($dependencyOf[$From])) {
            if ($step -eq $Wanted) { return $true }
            if (& $reaches $step $Wanted $Seen) { return $true }
        }

        return $false
    }

    $offer = New-Object -TypeName System.Collections.ArrayList

    foreach ($current in @($readable)) {
        $key = ([string] $current.Id).ToLowerInvariant()
        if ($key -eq $me) { continue }

        $blocked = [bool] (& $reaches $key $me (New-Object -TypeName 'System.Collections.Generic.HashSet[string]'))
        $reason = ''

        if ($blocked) {
            $reason = "'{0}' already depends on '{1}', so making it a dependency the other way round is a loop nothing can install." -f
            [string] $current.Name, (Get-HDTConsoleDisplayText -Text $nameOf[$me] -Fallback $Id)
        }

        [void] $offer.Add([pscustomobject] @{
                Id       = [string] $current.Id
                Display  = [string] $current.Name
                Selected = [bool] ($mine -contains $key)
                Blocked  = $blocked

                # THE SAME ANSWER THE OTHER WAY UP. The dialog binds IsEnabled
                # to this: a binding cannot negate, and a converter for one
                # boolean is a class in a module that loads markup with
                # XamlReader.
                Choosable = -not $blocked
                Reason   = $reason
            })
    }

    # NAME ORDER, so the list does not reshuffle as the share grows.
    return [pscustomobject[]] @(@($offer) | Sort-Object -Property Display)
}