Private/Objects.ps1

# What the cmdlets emit. Everything that mirrors an API payload is emitted as the API answers it (the same
# field names in the REST API, Cloud Shell and here), with a PSTypeName that picks its view: catalog types get
# their jsonFields parsed and their flatten objects' keys copied onto the item; deployments, their operations,
# what-if changes, diagnostics and tenants are typed for the views in VirtualMetric.Custom.Format.ps1xml, which
# show dates and positions readably without changing the data. The context is the one object the module makes
# itself (VirtualMetric.Context, see Private/Session.ps1).

# The API's timestamps are Unix seconds. Each such field gets a companion ScriptProperty with the local
# DateTime (createdAt -> CreatedAtTime), so a pipeline can compare with Get-Date. The getters are built unbound
# and self-contained: an object made in a thread job is read in another runspace.
$script:VMetricTimeGetters = [ordered]@{}
foreach ($timeField in @('createdAt', 'updatedAt', 'startedAt', 'completedAt')) {
    $script:VMetricTimeGetters[$timeField] = [scriptblock]::Create((@'
$value = $this.{0}
$seconds = 0L
if ($null -eq $value -or -not [long]::TryParse([string]$value, [ref] $seconds) -or $seconds -le 0) {{ return $null }}
if ($seconds -gt 100000000000) {{ return [DateTimeOffset]::FromUnixTimeMilliseconds($seconds).LocalDateTime }}
[DateTimeOffset]::FromUnixTimeSeconds($seconds).LocalDateTime
'@
 -f $timeField))
}

function Add-VMetricTimeProperty {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $InputObject
    )

    if (-not (Test-VMetricObjectLike -InputObject $InputObject) -or $InputObject -is [System.Collections.IDictionary]) {
        return
    }
    foreach ($field in $script:VMetricTimeGetters.Keys) {
        $property = $InputObject.psobject.Properties[$field]
        if (-not $property -or $property.MemberType -ne 'NoteProperty' -or $property.Value -is [string]) {
            continue
        }
        $name = $field.Substring(0, 1).ToUpperInvariant() + $field.Substring(1) + 'Time'
        if (-not $InputObject.psobject.Properties[$name]) {
            $InputObject.psobject.Properties.Add([System.Management.Automation.PSScriptProperty]::new($name, $script:VMetricTimeGetters[$field]))
        }
    }
}

function Add-VMetricTypeName {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $InputObject,

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

    if ($null -ne $InputObject -and $InputObject -is [System.Management.Automation.PSObject] -and $InputObject.psobject.TypeNames[0] -ne $TypeName) {
        $InputObject.psobject.TypeNames.Insert(0, $TypeName)
    }
}

function ConvertTo-VMetricOutputObject {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $InputObject,

        [AllowNull()]
        [AllowEmptyString()]
        [string] $TypeName
    )

    if ($null -eq $InputObject) {
        return
    }
    $object = $InputObject
    if ($object -is [System.Collections.IDictionary]) {
        $object = ConvertTo-VMetricPSObject -InputObject $object
    }
    if (-not (Test-VMetricObjectLike -InputObject $object)) {
        return $object
    }

    $type = Get-VMetricCatalogType -Name $TypeName
    if ($type) {
        foreach ($field in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $type -Name 'jsonFields'))) {
            $property = $object.psobject.Properties[[string]$field]
            if ($property -and $property.Value -is [string]) {
                $text = $property.Value.Trim()
                if ($text.StartsWith('{') -or $text.StartsWith('[')) {
                    try {
                        $property.Value = ConvertFrom-VMetricJson -Json $text
                    }
                    catch {
                        Write-Verbose "The $field field is not valid JSON; left as text."
                    }
                }
            }
        }
        foreach ($field in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $type -Name 'flatten'))) {
            $property = $object.psobject.Properties[[string]$field]
            if (-not $property -or -not (Test-VMetricObjectLike -InputObject $property.Value)) {
                continue
            }
            foreach ($entry in (Get-VMetricEntryList -InputObject $property.Value)) {
                if (-not $object.psobject.Properties[$entry.Name]) {
                    $object.psobject.Properties.Add([System.Management.Automation.PSNoteProperty]::new($entry.Name, $entry.Value))
                }
            }
        }
    }
    Add-VMetricTimeProperty -InputObject $object
    if ($TypeName) {
        Add-VMetricTypeName -InputObject $object -TypeName $TypeName
    }
    if ($TypeName -eq 'VirtualMetric.Deployment') {
        # Whoever emits a deployment (Get-VMetricDeployment, New-VMetricDeployment, a write's read back), its
        # operations and their property changes are typed for their views, with their timestamps.
        foreach ($operation in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $object -Name 'operations'))) {
            Add-VMetricTypeName -InputObject $operation -TypeName 'VirtualMetric.DeploymentOperation'
            Add-VMetricTimeProperty -InputObject $operation
            Add-VMetricDeltaTypeName -InputObject $operation
        }
    }
    $object
}

function Get-VMetricDeploymentTypeName {
    # The type the catalog gives deployments (Get-VMetricDeployment's output), so that New-VMetricDeployment and
    # Get-VMetricDeployment emit one shape.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    try {
        $entry = Get-VMetricCatalogCmdlet -Name 'Get-VMetricDeployment'
        $output = [string](Get-VMetricMember -InputObject $entry -Name 'output')
        if ($output) {
            return $output
        }
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        Write-Verbose "No catalog entry for Get-VMetricDeployment: $($_.Exception.Message)"
    }
    return 'VirtualMetric.Deployment'
}

function Add-VMetricDeltaTypeName {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $InputObject
    )

    foreach ($delta in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $InputObject -Name 'delta'))) {
        Add-VMetricTypeName -InputObject $delta -TypeName 'VirtualMetric.WhatIfPropertyChange'
    }
}

function ConvertTo-VMetricDeploymentObject {
    # A deployment as the API answers it, typed: the deployment, each operation
    # (VirtualMetric.DeploymentOperation) and each property change (VirtualMetric.WhatIfPropertyChange).
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $InputObject
    )

    $typeName = Get-VMetricDeploymentTypeName
    $object = ConvertTo-VMetricOutputObject -InputObject $InputObject -TypeName $typeName
    if ($typeName -ne 'VirtualMetric.Deployment') {
        foreach ($operation in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $object -Name 'operations'))) {
            Add-VMetricTypeName -InputObject $operation -TypeName 'VirtualMetric.DeploymentOperation'
            Add-VMetricTimeProperty -InputObject $operation
            Add-VMetricDeltaTypeName -InputObject $operation
        }
    }
    $object
}

function ConvertTo-VMetricWhatIfChange {
    # One change of a what-if as the API answers it, typed VirtualMetric.WhatIfChange.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $InputObject
    )

    Add-VMetricTypeName -InputObject $InputObject -TypeName 'VirtualMetric.WhatIfChange'
    Add-VMetricDeltaTypeName -InputObject $InputObject
    $InputObject
}

function ConvertTo-VMetricDiagnostic {
    # One diagnostic as the API answers it ({code, message, severity, range}), typed VirtualMetric.Diagnostic.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $InputObject
    )

    Add-VMetricTypeName -InputObject $InputObject -TypeName 'VirtualMetric.Diagnostic'
    $InputObject
}

function ConvertTo-VMetricTenantObject {
    # A tenant of GET /cli/tenants ({id, name, isMain, delegated}), typed VirtualMetric.Tenant, with isCurrent.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $InputObject,

        [AllowNull()]
        [AllowEmptyString()]
        [string] $CurrentTenantId
    )

    $object = ConvertTo-VMetricOutputObject -InputObject $InputObject -TypeName 'VirtualMetric.Tenant'
    $id = [string](Get-VMetricMember -InputObject $object -Name 'id')
    $object | Add-Member -NotePropertyName 'isCurrent' -NotePropertyValue ([bool]($CurrentTenantId -and $id -eq $CurrentTenantId)) -Force
    $object
}