Private/CatalogCmdlet.ps1

# Invoke-VMetricCatalogCmdlet: the body of every generated cmdlet. It implements the catalog's generic
# semantics (contract section 3.2), which Cloud Shell implements identically:
#
# get item with the id (the ById set), else the list, page by page (itemCount=100) until total; an
# exactName filter keeps only items whose name equals the value, ignoring case.
# new properties from the params' targets and transforms, deployed through the engine as a one-resource
# deployment (POST /deployments/resources?wait=true), then read back through the get cmdlet.
# set the current template form (POST /deployments/resources/read), the params applied onto a copy,
# deployed the same way.
# enable, disable
# set with properties.status active or passive.
# remove the id (-Id, or -Name through resolveIdBy), ShouldProcess (ConfirmImpact High), then DELETE.
# action a REST call with no engine involved, with ShouldProcess when it changes state.
#
# -WhatIf and -Confirm on new, set, enable and disable show the engine's own preview
# (POST /deployments/resources/preview): what it would actually change.

function Test-VMetricBound {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [string] $Name
    )

    foreach ($key in $Bound.Keys) {
        if ([string]$key -eq $Name) {
            return $true
        }
    }
    return $false
}

function Get-VMetricPathPlaceholder {
    # The {names} in a path template, in order.
    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [AllowNull()]
        [AllowEmptyString()]
        [string] $Template
    )

    $names = [System.Collections.Generic.List[string]]::new()
    if ($Template) {
        foreach ($match in [regex]::Matches($Template, '\{([^}]+)\}')) {
            $names.Add($match.Groups[1].Value)
        }
    }
    return , $names.ToArray()
}

function ConvertTo-VMetricPathSegment {
    # A value as one segment of a request path, URL-encoded. EscapeDataString leaves '.' and '..' as they are,
    # and .NET then collapses them (/pipelines/5/children/.. is sent as /pipelines/5/), so a request aimed at
    # a child would reach its parent: those two, and an empty value, are refused before any request is made.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [AllowNull()]
        [object] $Value,

        # What the value is, for the error: -Id, {childId}.
        [Parameter(Mandatory)]
        [string] $Name
    )

    $text = [string]$Value
    if ($text.Trim() -in @('', '.', '..')) {
        $shown = ConvertTo-VMetricSafeText -Text $text
        throw (New-VMetricException -Message "$Name cannot be '$shown': a value that names a resource in the request path may not be empty, '.' or '..'." `
                -Code 'InvalidPathValue' -Category InvalidArgument)
    }
    [System.Uri]::EscapeDataString($text)
}

function Resolve-VMetricPathTemplate {
    # A path template with its placeholders filled, each value one URL-encoded segment
    # (ConvertTo-VMetricPathSegment); $null when one has no value.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Template,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [hashtable] $Values
    )

    $builder = [System.Text.StringBuilder]::new()
    $position = 0
    foreach ($match in [regex]::Matches($Template, '\{([^}]+)\}')) {
        [void]$builder.Append($Template.Substring($position, $match.Index - $position))
        $name = $match.Groups[1].Value
        $value = $null
        if ($Values.ContainsKey($name)) {
            $value = $Values[$name]
        }
        if ($null -eq $value -or [string]$value -eq '') {
            return $null
        }
        [void]$builder.Append((ConvertTo-VMetricPathSegment -Value $value -Name "{$name}"))
        $position = $match.Index + $match.Length
    }
    [void]$builder.Append($Template.Substring($position))
    $builder.ToString()
}

function Resolve-VMetricRestArgument {
    # The bound params of a REST kind, each through its transform, by where it goes: path values by key, the
    # query, and the body (a dotted key nests).
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound
    )

    $pathValues = @{}
    $query = [ordered]@{}
    $body = [ordered]@{}
    $params = Get-VMetricCatalogParameter -Entry $Entry
    # A path value given as it is sent is checked before anything is sent (a transform's name is looked up
    # first; the id it resolves to is checked where the path is built).
    foreach ($param in $params) {
        $name = [string](Get-VMetricMember -InputObject $param -Name 'name')
        if ([string](Get-VMetricMember -InputObject $param -Name 'in') -eq 'path' -and -not (Get-VMetricMember -InputObject $param -Name 'transform') -and
            (Test-VMetricBound -Bound $Bound -Name $name)) {
            $null = ConvertTo-VMetricPathSegment -Value $Bound[$name] -Name "-$name"
        }
    }
    foreach ($param in $params) {
        $name = [string](Get-VMetricMember -InputObject $param -Name 'name')
        $location = [string](Get-VMetricMember -InputObject $param -Name 'in')
        if (-not $location -or -not (Test-VMetricBound -Bound $Bound -Name $name)) {
            continue
        }
        $key = [string](Get-VMetricMember -InputObject $param -Name 'key')
        if (-not $key) {
            $key = $name
        }
        $value = Resolve-VMetricParamValue -Param $param -Value $Bound[$name]
        if ($location -eq 'path') {
            $pathValues[$key] = $value
        }
        elseif ($location -eq 'query') {
            $query[$key] = $value
        }
        elseif ($location -eq 'body') {
            Set-VMetricPathValue -Target $body -Path $key -Value $value
        }
    }
    [pscustomobject]@{
        Path  = $pathValues
        Query = $query
        Body  = $body
    }
}

function Get-VMetricTypeField {
    # A catalog type's idField or nameField, with its default.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [AllowNull()]
        [AllowEmptyString()]
        [string] $TypeName,

        [Parameter(Mandatory)]
        [ValidateSet('idField', 'nameField')]
        [string] $Field
    )

    $type = Get-VMetricCatalogType -Name $TypeName
    $value = [string](Get-VMetricMember -InputObject $type -Name $Field)
    if ($value) {
        return $value
    }
    if ($Field -eq 'idField') {
        return 'id'
    }
    return 'name'
}

function Invoke-VMetricCatalogList {
    # Every item of a get cmdlet's list, page by page: itemCount=100 and pageNumber from 1 until total is
    # reached (or a short or empty page, or a page of items already seen when the endpoint ignores paging).
    # -NameFilter keeps only items whose name equals it, ignoring case.
    #
    # Every page is read before the first item is written. The cmdlet the items are piped to may change the
    # list as it runs (Get-VMetricDevice | Remove-VMetricDevice deletes; Get-VMetricDevice -Status passive |
    # Enable-VMetricDevice moves each item out of the filter), and a page read after that would start past items
    # nobody read: 250 matching, 150 removed. What list.expand reads per item is read as the item is written,
    # so Select-Object -First 1 still reads one.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [hashtable] $PathValues = @{},

        [System.Collections.IDictionary] $Query,

        [AllowNull()]
        [AllowEmptyString()]
        [string] $NameFilter,

        # Raw API items instead of typed objects.
        [switch] $Raw,

        # The public cmdlet, whose warning stream says an item was skipped.
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $list = Get-VMetricMember -InputObject $Entry -Name 'list'
    $template = [string](Get-VMetricMember -InputObject $list -Name 'path')
    $path = Resolve-VMetricPathTemplate -Template $template -Values $PathValues
    if ($null -eq $path) {
        $needed = (Get-VMetricPathPlaceholder -Template $template) -join ', '
        throw (New-VMetricException -Message "$($Entry.name) needs a value for: $needed." -Code 'MissingParameter' -Category InvalidArgument)
    }
    $method = [string](Get-VMetricMember -InputObject $list -Name 'method')
    if (-not $method) {
        $method = 'GET'
    }
    $itemsField = [string](Get-VMetricMember -InputObject $list -Name 'itemsField')
    $totalField = [string](Get-VMetricMember -InputObject $list -Name 'totalField')
    $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output')
    $idField = Get-VMetricTypeField -TypeName $output -Field 'idField'
    $nameField = Get-VMetricTypeField -TypeName $output -Field 'nameField'

    # list.expand: the list answers summaries (a device without its properties), so each item that passes the
    # name filter is read again through the item endpoint before it is written, one at a time. Resolving names
    # to ids (-Raw) needs only the summary.
    $expandTemplate = $null
    $expandKey = $null
    $expandMethod = 'GET'
    $itemDefinition = Get-VMetricMember -InputObject $Entry -Name 'item'
    if (-not $Raw -and [bool](Get-VMetricMember -InputObject $list -Name 'expand') -and $itemDefinition) {
        $expandTemplate = [string](Get-VMetricMember -InputObject $itemDefinition -Name 'path')
        foreach ($placeholder in (Get-VMetricPathPlaceholder -Template $expandTemplate)) {
            if (-not $PathValues.ContainsKey($placeholder)) {
                $expandKey = $placeholder
            }
        }
        $itemMethod = [string](Get-VMetricMember -InputObject $itemDefinition -Name 'method')
        if ($itemMethod) {
            $expandMethod = $itemMethod
        }
        if (-not $expandKey) {
            $expandTemplate = $null
        }
    }

    # Paging: with a totalField the list is read 100 at a time until total; without one it is read once.
    $paged = [bool]$totalField
    $pageSize = 100
    $pageNumber = 1
    $received = 0
    $seen = [System.Collections.Generic.HashSet[string]]::new()
    $listed = [System.Collections.Generic.List[object]]::new()
    while ($true) {
        $pageQuery = [ordered]@{}
        if ($Query) {
            foreach ($key in $Query.Keys) {
                $pageQuery[$key] = $Query[$key]
            }
        }
        if ($paged) {
            $pageQuery['pageNumber'] = $pageNumber
            $pageQuery['itemCount'] = $pageSize
        }
        $answer = Invoke-VMetricRequest -Method $method -Path $path -Query $pageQuery

        $items = $null
        if ($itemsField) {
            $items = Get-VMetricMember -InputObject $answer -Name $itemsField
        }
        if ($null -eq $items -and $answer -is [array]) {
            $items = $answer
        }
        $items = ConvertTo-VMetricArray -InputObject $items
        $total = $null
        if ($totalField) {
            $totalValue = Get-VMetricMember -InputObject $answer -Name $totalField
            if ($null -ne $totalValue) {
                $total = [long]$totalValue
            }
        }

        $fresh = 0
        foreach ($item in $items) {
            $id = [string](Get-VMetricMember -InputObject $item -Name $idField)
            if ($id -and -not $seen.Add($id)) {
                continue
            }
            $fresh++
            if ($NameFilter -and [string](Get-VMetricMember -InputObject $item -Name $nameField) -ne $NameFilter) {
                continue
            }
            $listed.Add($item)
        }

        $received += $items.Count
        if (-not $paged -or $items.Count -eq 0 -or $fresh -eq 0) {
            break
        }
        if ($null -ne $total -and $received -ge $total) {
            break
        }
        if ($null -eq $total -and $items.Count -lt $pageSize) {
            break
        }
        $pageNumber++
    }

    $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name)
    foreach ($item in $listed) {
        if ($Raw) {
            $item
            continue
        }
        $id = [string](Get-VMetricMember -InputObject $item -Name $idField)
        if ($expandTemplate -and $id) {
            $values = @{}
            foreach ($key in $PathValues.Keys) {
                $values[$key] = $PathValues[$key]
            }
            $values[$expandKey] = $id
            try {
                $full = Invoke-VMetricRequest -Method $expandMethod -Path (Resolve-VMetricPathTemplate -Template $expandTemplate -Values $values)
            }
            catch {
                if (Test-VMetricFlowControl -ErrorRecord $_) {
                    throw
                }
                if ($_.Exception -is [VMetricException] -and $_.Exception.StatusCode -eq 404) {
                    $text = "Skipped $noun $(ConvertTo-VMetricSafeText -Text $id): it was removed after the list was read."
                    if ($Cmdlet) {
                        $Cmdlet.WriteWarning($text)
                    }
                    else {
                        Write-Warning $text
                    }
                    continue
                }
                throw
            }
            $item = Join-VMetricObject -Base $item -Overlay $full
        }
        ConvertTo-VMetricOutputObject -InputObject $item -TypeName $output
    }
}

function Join-VMetricObject {
    # The item read through item laid over its list summary: the item's fields win, and what only the
    # summary has (a name the list resolves, say) stays.
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $Base,

        [AllowNull()]
        [object] $Overlay
    )

    if (-not (Test-VMetricObjectLike -InputObject $Overlay)) {
        return $Base
    }
    if (-not (Test-VMetricObjectLike -InputObject $Base)) {
        return $Overlay
    }
    $map = [ordered]@{}
    foreach ($entry in (Get-VMetricEntryList -InputObject $Base)) {
        $map[$entry.Name] = $entry.Value
    }
    foreach ($entry in (Get-VMetricEntryList -InputObject $Overlay)) {
        $key = Get-VMetricDictionaryKey -Dictionary $map -Name $entry.Name
        if ($null -eq $key) {
            $key = $entry.Name
        }
        $map[$key] = $entry.Value
    }
    [pscustomobject]$map
}

function Invoke-VMetricCatalogGet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound
    $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output')
    $item = Get-VMetricMember -InputObject $Entry -Name 'item'
    $list = Get-VMetricMember -InputObject $Entry -Name 'list'

    if ($item) {
        $itemTemplate = [string](Get-VMetricMember -InputObject $item -Name 'path')
        $itemPath = Resolve-VMetricPathTemplate -Template $itemTemplate -Values $arguments.Path
        $hasPlaceholders = (Get-VMetricPathPlaceholder -Template $itemTemplate).Count -gt 0
        if (($null -ne $itemPath -and $hasPlaceholders) -or -not $list) {
            if ($null -eq $itemPath) {
                $needed = (Get-VMetricPathPlaceholder -Template $itemTemplate) -join ', '
                throw (New-VMetricException -Message "$($Entry.name) needs a value for: $needed." -Code 'MissingParameter' -Category InvalidArgument)
            }
            $method = [string](Get-VMetricMember -InputObject $item -Name 'method')
            if (-not $method) {
                $method = 'GET'
            }
            $answer = Invoke-VMetricRequest -Method $method -Path $itemPath -Query $arguments.Query
            ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output
            return
        }
    }
    if (-not $list) {
        throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has neither a list nor an item." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating)
    }

    $nameFilter = $null
    foreach ($param in (Get-VMetricCatalogParameter -Entry $Entry)) {
        $name = [string](Get-VMetricMember -InputObject $param -Name 'name')
        if ((Get-VMetricMember -InputObject $param -Name 'filter') -eq 'exactName' -and (Test-VMetricBound -Bound $Bound -Name $name)) {
            $nameFilter = [string]$Bound[$name]
        }
    }
    Invoke-VMetricCatalogList -Entry $Entry -PathValues $arguments.Path -Query $arguments.Query -NameFilter $nameFilter -Cmdlet $Cmdlet
}

function Resolve-VMetricItemPath {
    # Fills an item path: from the bound path params, and the one placeholder left from -Name, looked up
    # through the entry's resolveIdBy get cmdlet (exact name; ambiguous or unknown names are errors).
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [string] $Template,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [hashtable] $PathValues
    )

    $values = @{}
    foreach ($key in $PathValues.Keys) {
        $values[$key] = $PathValues[$key]
    }
    $placeholders = Get-VMetricPathPlaceholder -Template $Template
    $missing = [System.Collections.Generic.List[string]]::new()
    foreach ($placeholder in $placeholders) {
        if (-not $values.ContainsKey($placeholder) -or [string]$values[$placeholder] -eq '') {
            $missing.Add($placeholder)
        }
    }
    $name = $null
    if (Test-VMetricBound -Bound $Bound -Name 'Name') {
        $name = [string]$Bound['Name']
    }
    $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name)

    if ($missing.Count -eq 1 -and $name) {
        $resolver = [string](Get-VMetricMember -InputObject $Entry -Name 'resolveIdBy')
        if (-not $resolver) {
            throw (New-VMetricException -Message "$($Entry.name) cannot find a $noun by name; use -Id." -Code 'CatalogEntryInvalid' -Category InvalidData)
        }
        $getEntry = Get-VMetricCatalogCmdlet -Name $resolver
        # The parameters the resolver shares with this cmdlet (a child's -Pipeline) go to it as its own
        # parameters, through its own definitions; its exact-name filter takes -Name.
        $resolverBound = @{}
        foreach ($param in (Get-VMetricCatalogParameter -Entry $getEntry)) {
            $paramName = [string](Get-VMetricMember -InputObject $param -Name 'name')
            if ($paramName -notin @('Name', 'Id') -and (Test-VMetricBound -Bound $Bound -Name $paramName)) {
                $resolverBound[$paramName] = $Bound[$paramName]
            }
        }
        $resolverArguments = Resolve-VMetricRestArgument -Entry $getEntry -Bound $resolverBound
        $query = [ordered]@{}
        foreach ($key in $resolverArguments.Query.Keys) {
            $query[$key] = $resolverArguments.Query[$key]
        }
        foreach ($param in (Get-VMetricCatalogParameter -Entry $getEntry)) {
            if ((Get-VMetricMember -InputObject $param -Name 'filter') -eq 'exactName' -and (Get-VMetricMember -InputObject $param -Name 'in') -eq 'query') {
                $query[[string](Get-VMetricMember -InputObject $param -Name 'key')] = $name
            }
        }
        $found = @(Invoke-VMetricCatalogList -Entry $getEntry -PathValues $resolverArguments.Path -Query $query -NameFilter $name -Raw)
        $idField = Get-VMetricTypeField -TypeName ([string](Get-VMetricMember -InputObject $getEntry -Name 'output')) -Field 'idField'
        if ($found.Count -eq 0) {
            throw (New-VMetricException -Message "No $noun named '$(ConvertTo-VMetricSafeText -Text $name)' was found." -Code 'ResourceNotFound' -Category ObjectNotFound)
        }
        if ($found.Count -gt 1) {
            $ids = ($found | ForEach-Object { ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $_ -Name $idField)) }) -join ', '
            throw (New-VMetricException -Message "Several ${noun}s are named '$(ConvertTo-VMetricSafeText -Text $name)' ($ids); use -Id." -Code 'AmbiguousName' -Category InvalidArgument)
        }
        $values[$missing[0]] = [string](Get-VMetricMember -InputObject $found[0] -Name $idField)
        $missing.Clear()
    }
    if ($missing.Count -gt 0) {
        throw (New-VMetricException -Message "$($Entry.name) needs -Id or -Name." -Code 'MissingParameter' -Category InvalidArgument)
    }

    $id = $null
    if ($placeholders.Count -gt 0) {
        $id = [string]$values[$placeholders[$placeholders.Count - 1]]
    }
    # The label a confirmation names the resource by: the id may be one a list answered.
    $shownName = ConvertTo-VMetricSafeText -Text $name
    $shownId = ConvertTo-VMetricSafeText -Text $id
    $label = $noun
    if ($name -and $id) {
        $label = "$noun '$shownName' ($shownId)"
    }
    elseif ($name) {
        $label = "$noun '$shownName'"
    }
    elseif ($id) {
        $label = "$noun $shownId"
    }
    [pscustomobject]@{
        Path  = Resolve-VMetricPathTemplate -Template $Template -Values $values
        Name  = $name
        Id    = $id
        Label = $label
    }
}

function Test-VMetricShouldProcess {
    # $Cmdlet.ShouldProcess, behind a seam: with -Description the three-argument form (the what-if text, the
    # confirmation question and its caption), otherwise the target and action form.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Asks ShouldProcess of the public cmdlet it is handed, which declares SupportsShouldProcess.')]
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        [string] $Target,

        [string] $Action,

        [string] $Description,

        [string] $Warning,

        [string] $Caption
    )

    if ($Description) {
        return $Cmdlet.ShouldProcess($Description, $Warning, $Caption)
    }
    if ($Action) {
        return $Cmdlet.ShouldProcess($Target, $Action)
    }
    return $Cmdlet.ShouldProcess($Target)
}

function Get-VMetricConfirmImpact {
    # The ConfirmImpact the public cmdlet declares (its CmdletBinding), else the catalog's, else Medium.
    [CmdletBinding()]
    [OutputType([System.Management.Automation.ConfirmImpact])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        [AllowNull()]
        [object] $Entry
    )

    $command = $Cmdlet.MyInvocation.MyCommand
    if ($command -is [System.Management.Automation.FunctionInfo] -and $command.ScriptBlock) {
        foreach ($attribute in $command.ScriptBlock.Attributes) {
            if ($attribute -is [System.Management.Automation.CmdletBindingAttribute]) {
                return $attribute.ConfirmImpact
            }
        }
    }
    $fromCatalog = [string](Get-VMetricMember -InputObject $Entry -Name 'confirmImpact')
    if ($fromCatalog) {
        return [System.Management.Automation.ConfirmImpact]$fromCatalog
    }
    return [System.Management.Automation.ConfirmImpact]::Medium
}

function Test-VMetricWhatIf {
    # Whether the public cmdlet runs with -WhatIf: given to it, or $WhatIfPreference. (Its -WhatIf is read
    # from its bound parameters: $PSCmdlet.GetVariableValue does not see the cmdlet's own scope from here.)
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $bound = $Cmdlet.MyInvocation.BoundParameters
    if ($bound.ContainsKey('WhatIf')) {
        return [bool]$bound['WhatIf']
    }
    return [bool]$WhatIfPreference
}

function Test-VMetricPromptActive {
    # Whether ShouldProcess will print a what-if or ask for confirmation: only then is the preview worth its
    # round trip.
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        [Parameter(Mandatory)]
        [System.Management.Automation.ConfirmImpact] $ConfirmImpact
    )

    if (Test-VMetricWhatIf -Cmdlet $Cmdlet) {
        return $true
    }
    $bound = $Cmdlet.MyInvocation.BoundParameters
    if ($bound.ContainsKey('Confirm')) {
        return [bool]$bound['Confirm']
    }
    $preference = [System.Management.Automation.ConfirmImpact]$ConfirmPreference
    return ($preference -ne [System.Management.Automation.ConfirmImpact]::None -and [int]$ConfirmImpact -ge [int]$preference)
}

function New-VMetricResourceDeploymentName {
    # pwsh-<verb>-<type's last segment>-<resource name>, at most 128 characters, no control characters.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds a string; changes no state.')]
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Verb,

        [Parameter(Mandatory)]
        [string] $ResourceType,

        [Parameter(Mandatory)]
        [string] $Name
    )

    $segment = $ResourceType.Substring($ResourceType.LastIndexOf('/') + 1)
    $text = [regex]::Replace("pwsh-$($Verb.ToLowerInvariant())-$segment-$Name", '[\p{C}\p{Zl}\p{Zp}]', '')
    if ($text.Length -gt 128) {
        $text = $text.Substring(0, 128)
    }
    $text
}

function Format-VMetricPreviewText {
    # The engine's preview as the what-if shows it: for new, one line per change ("create <type> <name>");
    # otherwise every delta as "<path>: <before> -> <after>". Secrets the request carries are masked whatever the
    # server echoes: a delta at a secret's own path (SecretPath) shows ***, and every value is masked before it
    # is encoded as JSON (Hide-VMetricSecretValue).
    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [AllowNull()]
        [object] $Preview,

        [Parameter(Mandatory)]
        [string] $Kind,

        [AllowNull()]
        [string[]] $Secret,

        # Where the request's resource holds its secrets (Get-VMetricSecretPath).
        [AllowNull()]
        [string[]] $SecretPath
    )

    $arrow = [string][char]0x2192
    $lines = [System.Collections.Generic.List[string]]::new()
    foreach ($change in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Preview -Name 'changes'))) {
        $changeType = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'changeType'))
        $type = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'type'))
        $name = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'name'))
        $deltas = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $change -Name 'delta')
        if ($Kind -eq 'new' -or $deltas.Count -eq 0) {
            $lines.Add("$changeType $type $name")
        }
        if ($Kind -ne 'new') {
            foreach ($delta in $deltas) {
                $rawPath = [string](Get-VMetricMember -InputObject $delta -Name 'path')
                $path = ConvertTo-VMetricSafeText -Text $rawPath
                $before = Format-VMetricValue -InputObject (Hide-VMetricSecretValue -InputObject (Get-VMetricMember -InputObject $delta -Name 'before') -Secret $Secret -Path $rawPath -SecretPath $SecretPath)
                $after = Format-VMetricValue -InputObject (Hide-VMetricSecretValue -InputObject (Get-VMetricMember -InputObject $delta -Name 'after') -Secret $Secret -Path $rawPath -SecretPath $SecretPath)
                $lines.Add("${path}: $before $arrow $after")
            }
        }
        $failure = [string](Get-VMetricMember -InputObject $change -Name 'error')
        if ($failure) {
            $lines.Add(" error: $(ConvertTo-VMetricSafeText -Text $failure)")
        }
    }
    $masked = foreach ($line in $lines) {
        Hide-VMetricSecretText -Text $line -Secret $Secret
    }
    return , [string[]]@($masked)
}

function Write-VMetricPreviewDiagnostic {
    # A preview's diagnostics through the public cmdlet: errors as errors, warnings as warnings. Answers
    # whether there was an error (the deployment would be refused).
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        [AllowNull()]
        [object] $Preview,

        [AllowNull()]
        [string[]] $Secret,

        [AllowNull()]
        [object] $TargetObject
    )

    $hasError = $false
    foreach ($diagnostic in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Preview -Name 'diagnostics'))) {
        $severity = [string](Get-VMetricMember -InputObject $diagnostic -Name 'severity')
        $code = [string](Get-VMetricMember -InputObject $diagnostic -Name 'code')
        $message = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $diagnostic -Name 'message'))
        $text = Hide-VMetricSecretText -Text "$(ConvertTo-VMetricSafeText -Text $code): $message" -Secret $Secret
        if ($severity -eq 'error') {
            $hasError = $true
            $exception = New-VMetricException -Message $text -Code $(if ($code) { $code } else { 'PreviewError' }) -Category InvalidData
            $Cmdlet.WriteError((New-VMetricErrorRecord -Exception $exception -TargetObject $TargetObject))
        }
        elseif ($severity -eq 'warning') {
            $Cmdlet.WriteWarning($text)
        }
    }
    return $hasError
}

function Wait-VMetricDeploymentCompletion {
    # Polls GET /deployments/{id} every IntervalSec seconds while the deployment runs, with Write-Progress per
    # operation and a verbose line as each operation finishes. Answers the final deployment.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Deployment,

        [System.Collections.IDictionary] $Session,

        [double] $IntervalSec = 2,

        [int] $TimeoutMinutes = 45
    )

    $id = [string](Get-VMetricMember -InputObject $Deployment -Name 'id')
    $shownId = ConvertTo-VMetricSafeText -Text $id
    $name = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $Deployment -Name 'name'))
    $activity = "Deployment '$name'"
    $reported = @{}
    $deadline = [datetime]::UtcNow.AddMinutes($TimeoutMinutes)
    $request = @{ Method = 'GET'; Path = "/deployments/$(ConvertTo-VMetricPathSegment -Value $id -Name 'The deployment id')" }
    if ($Session) {
        $request.Session = $Session
    }
    try {
        while ($true) {
            $operations = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Deployment -Name 'operations')
            $finished = 0
            $running = $null
            foreach ($operation in $operations) {
                $status = [string](Get-VMetricMember -InputObject $operation -Name 'status')
                $seq = [string](Get-VMetricMember -InputObject $operation -Name 'seq')
                if ($status -in @('succeeded', 'failed', 'skipped')) {
                    $finished++
                    if (-not $reported.ContainsKey($seq)) {
                        $reported[$seq] = $true
                        Write-Verbose ('[{0}/{1}] {2} {3} ''{4}'': {5}' -f (ConvertTo-VMetricSafeText -Text $seq), $operations.Count,
                            (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'operation'))),
                            (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'type'))),
                            (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'name'))), $status)
                    }
                }
                elseif ($status -eq 'running' -and -not $running) {
                    $running = $operation
                }
            }
            if ([string](Get-VMetricMember -InputObject $Deployment -Name 'status') -ne 'running') {
                return $Deployment
            }
            if ([datetime]::UtcNow -ge $deadline) {
                throw (New-VMetricException -Message "Deployment '$name' ($shownId) is still running after $TimeoutMinutes minutes. Follow it with Get-VMetricDeployment -Id $shownId." `
                        -Code 'DeploymentStillRunning' -Category OperationTimeout)
            }

            $percent = 0
            if ($operations.Count -gt 0) {
                $percent = [int][Math]::Floor(100 * $finished / $operations.Count)
            }
            $progress = @{
                Activity        = $activity
                Status          = "$finished of $($operations.Count) operations complete"
                PercentComplete = $percent
            }
            if ($running) {
                $progress.CurrentOperation = '{0} {1} ''{2}''' -f (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'operation'))),
                (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'type'))),
                (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'name')))
            }
            Write-Progress @progress
            Wait-VMetricInterval -Seconds $IntervalSec
            $Deployment = Invoke-VMetricRequest @request
        }
    }
    finally {
        Write-Progress -Activity $activity -Completed
    }
}

function Add-VMetricOneTimeOutput {
    # A write's one-time outputs (a new director's apiKey and installScript) on the emitted object, as ApiKey and
    # InstallScript, with a warning: they are shown once.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        [Parameter(Mandatory)]
        [object] $InputObject,

        [AllowNull()]
        [object] $Outputs,

        [Parameter(Mandatory)]
        [string] $Label
    )

    $names = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in (Get-VMetricEntryList -InputObject $Outputs)) {
        if (-not $entry.Name) {
            continue
        }
        $name = $entry.Name.Substring(0, 1).ToUpperInvariant() + $entry.Name.Substring(1)
        $InputObject | Add-Member -NotePropertyName $name -NotePropertyValue $entry.Value -Force
        $names.Add((ConvertTo-VMetricSafeText -Text $name))
    }
    if ($names.Count -gt 0) {
        $Cmdlet.WriteWarning("$Label returned $($names -join ' and '): shown once; copy it now. It cannot be shown again.")
    }
}

function Get-VMetricWrittenResource {
    # The resource a write created or changed, read back through the get cmdlet's item by the id in the
    # operation record. Falls back to the template form when there is no such item.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [AllowNull()]
        [AllowEmptyString()]
        [string] $ResourceId,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Resource
    )

    $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output')
    $getEntry = Find-VMetricReadCmdlet -Entry $Entry
    $item = Get-VMetricMember -InputObject $getEntry -Name 'item'
    if ($item -and $ResourceId) {
        $template = [string](Get-VMetricMember -InputObject $item -Name 'path')
        $placeholders = Get-VMetricPathPlaceholder -Template $template
        $values = @{}
        if ($placeholders.Count -eq 1) {
            $values[$placeholders[0]] = $ResourceId
        }
        elseif ($placeholders.Count -eq 2 -and $Resource.Contains('parent')) {
            $parent = $Resource['parent']
            $values[$placeholders[0]] = (Resolve-VMetricResourceId -Type $parent['type'] -Name @([string]$parent['name']))[0]
            $values[$placeholders[1]] = $ResourceId
        }
        $path = Resolve-VMetricPathTemplate -Template $template -Values $values
        if ($path) {
            if (-not $output) {
                $output = [string](Get-VMetricMember -InputObject $getEntry -Name 'output')
            }
            $answer = Invoke-VMetricRequest -Method GET -Path $path
            return (ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output)
        }
    }

    $lookup = [ordered]@{ type = $Resource['type']; name = $Resource['name'] }
    if ($Resource.Contains('parent')) {
        $lookup['parent'] = $Resource['parent']
    }
    $read = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($lookup) })
    $current = (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $read -Name 'resources')) | Select-Object -First 1
    $object = [ordered]@{
        id         = [string](Get-VMetricMember -InputObject $current -Name 'id')
        name       = $Resource['name']
        properties = Get-VMetricMember -InputObject $current -Name 'properties'
    }
    ConvertTo-VMetricOutputObject -InputObject $object -TypeName $output
}

function Format-VMetricResourceName {
    # A nameFormat such as "{Device}/{Target}" filled with the parameters' values as given.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Format,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [string] $CmdletName
    )

    $builder = [System.Text.StringBuilder]::new()
    $position = 0
    foreach ($match in [regex]::Matches($Format, '\{([^}]+)\}')) {
        [void]$builder.Append($Format.Substring($position, $match.Index - $position))
        $parameter = $match.Groups[1].Value
        if (-not (Test-VMetricBound -Bound $Bound -Name $parameter) -or [string]$Bound[$parameter] -eq '') {
            throw (New-VMetricException -Message "$CmdletName needs -$parameter." -Code 'MissingParameter' -Category InvalidArgument)
        }
        [void]$builder.Append([string]$Bound[$parameter])
        $position = $match.Index + $match.Length
    }
    [void]$builder.Append($Format.Substring($position))
    $builder.ToString()
}

function Invoke-VMetricCatalogWrite {
    # new, set, enable and disable: one resource through the deployment engine.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $kind = [string]$Entry.kind
    $resourceType = [string](Get-VMetricMember -InputObject $Entry -Name 'resourceType')
    if (-not $resourceType) {
        throw (New-VMetricException -Message "The catalog entry of $($Entry.name) names no resourceType." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating)
    }
    $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name)

    # The resource in the deployment property model, at the version the catalog was written for.
    $resource = [ordered]@{ type = $resourceType }
    $apiVersion = [string](Get-VMetricCatalog).ApiVersion
    if ($apiVersion) {
        $resource['apiVersion'] = $apiVersion
    }
    $resource['name'] = $null
    $resource['properties'] = [ordered]@{}
    $parentName = $null
    $parentType = $null
    $assignments = [System.Collections.Generic.List[object]]::new()
    foreach ($param in (Get-VMetricCatalogParameter -Entry $Entry)) {
        $name = [string](Get-VMetricMember -InputObject $param -Name 'name')
        $target = [string](Get-VMetricMember -InputObject $param -Name 'target')
        if (-not $target -or -not (Test-VMetricBound -Bound $Bound -Name $name)) {
            continue
        }
        $value = Resolve-VMetricParamValue -Param $param -Value $Bound[$name]
        if ($target -eq 'name') {
            $resource['name'] = [string]$value
        }
        elseif ($target -eq 'parent.name' -or $target -eq 'parent') {
            $parentName = [string]$value
        }
        elseif ($target -eq 'parent.type') {
            $parentType = [string]$value
        }
        else {
            $assignments.Add([pscustomobject]@{
                    Name      = $name
                    Target    = $target
                    Transform = [string](Get-VMetricMember -InputObject $param -Name 'transform')
                    Value     = $value
                })
        }
    }
    # nameFormat builds the name from parameters when none targets it: a quick route is '<device>/<target>',
    # from the names given, before they are resolved to ids.
    $nameFormat = [string](Get-VMetricMember -InputObject $Entry -Name 'nameFormat')
    if (-not $resource['name'] -and $nameFormat) {
        $resource['name'] = Format-VMetricResourceName -Format $nameFormat -Bound $Bound -CmdletName ([string]$Entry.name)
    }
    $resourceName = [string]$resource['name']
    if (-not $resourceName) {
        throw (New-VMetricException -Message "$($Entry.name) needs -Name." -Code 'MissingParameter' -Category InvalidArgument)
    }
    if ($parentName) {
        if (-not $parentType) {
            $parentType = [string](Get-VMetricMember -InputObject $Entry -Name 'parentType')
        }
        if (-not $parentType -and ($resourceType.Split('/').Count -gt 2)) {
            $parentType = $resourceType.Substring(0, $resourceType.LastIndexOf('/'))
        }
        $resource['parent'] = [ordered]@{ type = $parentType; name = $parentName }
    }
    # The name may be a server's, piped from a get: the label a prompt and every message show is cleaned.
    $shownName = ConvertTo-VMetricSafeText -Text $resourceName
    $label = "$noun '$shownName'"

    # The current state: set, enable and disable change it; new must not find it.
    $lookup = [ordered]@{ type = $resourceType; name = $resourceName }
    if ($resource.Contains('parent')) {
        $lookup['parent'] = $resource['parent']
    }
    $read = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($lookup) }) -AsHashtable
    $current = (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $read -Name 'resources')) | Select-Object -First 1
    $exists = [bool](Get-VMetricMember -InputObject $current -Name 'exists')
    if ($kind -eq 'new') {
        if ($exists) {
            $setName = 'Set-' + ([string]$Entry.name).Substring(([string]$Entry.name).IndexOf('-') + 1)
            throw (New-VMetricException -Message "A $noun named '$shownName' already exists. Use $setName to change it." -Code 'ResourceExists' -Category ResourceExists)
        }
    }
    else {
        if (-not $exists) {
            throw (New-VMetricException -Message "No $noun named '$shownName' was found." -Code 'ResourceNotFound' -Category ObjectNotFound)
        }
        $properties = Copy-VMetricData -InputObject (Get-VMetricMember -InputObject $current -Name 'properties')
        if ($properties -isnot [System.Collections.IDictionary]) {
            $properties = [ordered]@{}
        }
        $resource['properties'] = $properties
    }

    foreach ($assignment in $assignments) {
        if ($assignment.Transform -eq 'merge') {
            Merge-VMetricPathValue -Target $resource -Path $assignment.Target -Value $assignment.Value -ParameterName $assignment.Name
        }
        elseif ($assignment.Transform -like 'relationOf:*') {
            Set-VMetricRelationValue -Target $resource -Path $assignment.Target -Kind $assignment.Transform.Substring('relationOf:'.Length) -Value $assignment.Value
        }
        else {
            Set-VMetricPathValue -Target $resource -Path $assignment.Target -Value $assignment.Value
        }
    }
    if ($kind -eq 'enable') {
        Set-VMetricPathValue -Target $resource -Path 'properties.status' -Value 'active'
    }
    elseif ($kind -eq 'disable') {
        Set-VMetricPathValue -Target $resource -Path 'properties.status' -Value 'passive'
    }
    elseif ($kind -eq 'set' -and $assignments.Count -eq 0) {
        $Cmdlet.WriteWarning("Nothing to change on $label`: give at least one property.")
        return
    }

    $verbText = @{ new = 'Create'; set = 'Update'; enable = 'Enable'; disable = 'Disable' }[$kind]
    $description = "$verbText $label"
    $secrets = Get-VMetricSecretText -InputObject $resource
    $impact = Get-VMetricConfirmImpact -Cmdlet $Cmdlet -Entry $Entry
    if (Test-VMetricPromptActive -Cmdlet $Cmdlet -ConfirmImpact $impact) {
        $preview = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/preview' -Body ([ordered]@{ resources = @($resource) })
        $refused = Write-VMetricPreviewDiagnostic -Cmdlet $Cmdlet -Preview $preview -Secret $secrets -TargetObject $label
        $lines = Format-VMetricPreviewText -Preview $preview -Kind $kind -Secret $secrets -SecretPath (Get-VMetricSecretPath -InputObject $resource)
        $changeTypes = [System.Collections.Generic.List[string]]::new()
        foreach ($change in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $preview -Name 'changes'))) {
            $changeTypes.Add([string](Get-VMetricMember -InputObject $change -Name 'changeType'))
        }
        if ($kind -ne 'new' -and $changeTypes.Count -gt 0 -and -not ($changeTypes | Where-Object { $_ -ne 'noChange' })) {
            $description = "${description}: no change"
        }
        elseif ($lines.Count -gt 0) {
            $description = "${description}:" + [Environment]::NewLine + (($lines | ForEach-Object { " $_" }) -join [Environment]::NewLine)
        }
        if ($refused -and -not (Test-VMetricWhatIf -Cmdlet $Cmdlet)) {
            return
        }
    }
    $question = "$description$([Environment]::NewLine)Continue?"
    if (-not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Description $description -Warning $question -Caption "$verbText $noun")) {
        return
    }

    $deploymentName = New-VMetricResourceDeploymentName -Verb $kind -ResourceType $resourceType -Name $resourceName
    $deployment = Invoke-VMetricRequest -Method POST -Path '/deployments/resources' -Query ([ordered]@{ wait = 'true' }) `
        -Body ([ordered]@{ name = $deploymentName; resources = @($resource) })
    $deployment = Wait-VMetricDeploymentCompletion -Deployment $deployment

    $operations = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $deployment -Name 'operations')
    $operation = $null
    foreach ($candidate in $operations) {
        if ([string](Get-VMetricMember -InputObject $candidate -Name 'type') -eq $resourceType -and
            [string](Get-VMetricMember -InputObject $candidate -Name 'name') -eq $resourceName) {
            $operation = $candidate
        }
    }
    if (-not $operation -and $operations.Count -gt 0) {
        $operation = $operations[$operations.Count - 1]
    }

    $status = [string](Get-VMetricMember -InputObject $deployment -Name 'status')
    if ($status -ne 'succeeded') {
        $failure = Get-VMetricMember -InputObject $operation -Name 'error'
        if (-not $failure) {
            $failure = Get-VMetricMember -InputObject $deployment -Name 'error'
        }
        $code = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $failure -Name 'code'))
        if (-not $code) {
            $code = 'DEPLOYMENT_FAILED'
        }
        $message = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $failure -Name 'message'))
        if (-not $message) {
            $message = "the deployment ended $(ConvertTo-VMetricSafeText -Text $status)"
        }
        $masked = Hide-VMetricSecretText -Text $message -Secret $secrets
        $details = $deployment
        if ($masked -ne $message) {
            $message = $masked
            $details = $null
        }
        $deploymentId = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $deployment -Name 'id'))
        throw (New-VMetricException -Message "$verbText $label failed: $message ($code; deployment $deploymentName, id $deploymentId)" `
                -Code $code -Category InvalidOperation -Details $details)
    }

    $resourceId = [string](Get-VMetricMember -InputObject $operation -Name 'resourceId')
    try {
        $object = Get-VMetricWrittenResource -Entry $Entry -ResourceId $resourceId -Resource $resource
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        $Cmdlet.WriteWarning("$verbText $label succeeded, but reading it back failed: $($_.Exception.Message)")
        $object = ConvertTo-VMetricOutputObject -InputObject ([ordered]@{ id = $resourceId; name = $resourceName }) -TypeName ([string]$Entry.output)
    }
    if ($object) {
        Add-VMetricOneTimeOutput -Cmdlet $Cmdlet -InputObject $object -Outputs (Get-VMetricMember -InputObject $operation -Name 'outputs') -Label $label
        $object
    }
}

function Invoke-VMetricCatalogRemove {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $item = Get-VMetricMember -InputObject $Entry -Name 'item'
    $template = [string](Get-VMetricMember -InputObject $item -Name 'path')
    if (-not $template) {
        throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has no item path." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating)
    }
    $method = [string](Get-VMetricMember -InputObject $item -Name 'method')
    if (-not $method) {
        $method = 'DELETE'
    }
    $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound
    $target = Resolve-VMetricItemPath -Entry $Entry -Bound $Bound -Template $template -PathValues $arguments.Path
    if (-not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Target $target.Label -Action 'Remove')) {
        return
    }
    $null = Invoke-VMetricRequest -Method $method -Path $target.Path -Query $arguments.Query
}

function Invoke-VMetricCatalogAction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Entry,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $request = Get-VMetricMember -InputObject $Entry -Name 'action'
    foreach ($field in @('request', 'item')) {
        if (-not $request) {
            $request = Get-VMetricMember -InputObject $Entry -Name $field
        }
    }
    $template = [string](Get-VMetricMember -InputObject $request -Name 'path')
    if (-not $template) {
        throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has no request path." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating)
    }
    $method = [string](Get-VMetricMember -InputObject $request -Name 'method')
    if (-not $method) {
        $method = 'POST'
    }
    $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound
    $target = Resolve-VMetricItemPath -Entry $Entry -Bound $Bound -Template $template -PathValues $arguments.Path
    if ($method -ne 'GET' -and -not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Target $target.Label)) {
        return
    }

    $call = @{ Method = $method; Path = $target.Path; Query = $arguments.Query }
    if ($method -in @('POST', 'PUT', 'PATCH')) {
        $call.Body = $arguments.Body
    }
    $answer = Invoke-VMetricRequest @call
    if ($null -eq $answer) {
        return
    }
    $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output')
    $itemsField = [string](Get-VMetricMember -InputObject $request -Name 'itemsField')
    $items = $null
    if ($itemsField) {
        $items = Get-VMetricMember -InputObject $answer -Name $itemsField
    }
    elseif ($answer -is [array]) {
        $items = $answer
    }
    if ($null -ne $items) {
        foreach ($entry in (ConvertTo-VMetricArray -InputObject $items)) {
            ConvertTo-VMetricOutputObject -InputObject $entry -TypeName $output
        }
        return
    }
    ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output
}

function Select-VMetricPipelineBound {
    # The parameters the pipeline object being processed bound, with those given on the command line.
    # PowerShell keeps a value a previous object bound by property name in $PSBoundParameters when the next
    # object lacks the property (it resets only the variable): without this, Remove-VMetricDevice would target
    # the previous object's id under a prompt naming the current one, and New-VMetricDevice would copy the
    # previous object's port. A parameter that did not come from the command line stays only when it is bound
    # by value, or when the object has a property of its name or of one of its aliases.
    [CmdletBinding()]
    [OutputType([System.Collections.IDictionary])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        # The keys of $PSBoundParameters in the cmdlet's begin block.
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [string[]] $CommandLine,

        # The pipeline object being processed ($_ in the process block).
        [AllowNull()]
        [object] $InputObject,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet
    )

    $parameters = $Cmdlet.MyInvocation.MyCommand.Parameters
    $fresh = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($key in $Bound.Keys) {
        $name = [string]$key
        if ($CommandLine -contains $name) {
            $fresh[$name] = $Bound[$key]
            continue
        }
        $metadata = $null
        if ($parameters -and -not $parameters.TryGetValue($name, [ref] $metadata)) {
            $metadata = $null
        }
        if (-not $metadata) {
            continue
        }
        $byValue = $false
        foreach ($attribute in $metadata.Attributes) {
            if ($attribute -is [System.Management.Automation.ParameterAttribute] -and $attribute.ValueFromPipeline) {
                $byValue = $true
            }
        }
        $carried = $false
        if ($null -ne $InputObject) {
            foreach ($candidate in (@($metadata.Name) + @($metadata.Aliases))) {
                if ($InputObject -is [System.Collections.IDictionary]) {
                    if ($InputObject.Contains($candidate)) {
                        $carried = $true
                    }
                }
                elseif ($InputObject.psobject.Properties[$candidate]) {
                    $carried = $true
                }
            }
        }
        if ($byValue -or $carried) {
            $fresh[$name] = $Bound[$key]
        }
        else {
            Write-Verbose "-$name is not on this pipeline object; the previous object's value is not used."
        }
    }
    return , $fresh
}

function Invoke-VMetricCatalogCmdlet {
    # The body of every generated cmdlet: Invoke-VMetricCatalogCmdlet -Name '<Cmdlet>' -Bound $PSBoundParameters
    # -Cmdlet $PSCmdlet -CommandLine $commandLine -InputObject $_. Output, errors, warnings and ShouldProcess all
    # go through the public cmdlet, so they name it and honour its -WhatIf, -Confirm, -ErrorAction and
    # -WarningAction.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.IDictionary] $Bound,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet] $Cmdlet,

        # The parameters given on the command line (the stub's begin block); with the pipeline object below,
        # what tells this object's own values from those a previous object left behind.
        [AllowNull()]
        [AllowEmptyCollection()]
        [string[]] $CommandLine,

        [AllowNull()]
        [object] $InputObject
    )

    if ($null -ne $CommandLine) {
        $Bound = Select-VMetricPipelineBound -Bound $Bound -CommandLine $CommandLine -InputObject $InputObject -Cmdlet $Cmdlet
    }
    try {
        $entry = Get-VMetricCatalogCmdlet -Name $Name
        $kind = [string](Get-VMetricMember -InputObject $entry -Name 'kind')
        if ($kind -eq 'get') {
            Invoke-VMetricCatalogGet -Entry $entry -Bound $Bound -Cmdlet $Cmdlet
        }
        elseif ($kind -in @('new', 'set', 'enable', 'disable')) {
            Invoke-VMetricCatalogWrite -Entry $entry -Bound $Bound -Cmdlet $Cmdlet
        }
        elseif ($kind -eq 'remove') {
            Invoke-VMetricCatalogRemove -Entry $entry -Bound $Bound -Cmdlet $Cmdlet
        }
        elseif ($kind -eq 'action') {
            Invoke-VMetricCatalogAction -Entry $entry -Bound $Bound -Cmdlet $Cmdlet
        }
        else {
            throw (New-VMetricException -Message "$Name is of kind '$kind', which is not served by Invoke-VMetricCatalogCmdlet." `
                    -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating)
        }
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        $record = Hide-VMetricSecretInError -ErrorRecord $_ -Secret (Get-VMetricSecretText -InputObject $Bound)
        Write-VMetricCmdletError -Cmdlet $Cmdlet -ErrorRecord $record
    }
}