Private/Transport.ps1

$script:LaunchLibraryConnection = [ordered]@{
    BaseUri        = 'https://ll.thespacedevs.com'
    ApiKey         = $null
    UseDevelopment = $false
}

function ConvertTo-LaunchLibraryQueryString {
    [CmdletBinding()]
    param([hashtable]$Query)

    if (-not $Query -or $Query.Count -eq 0) { return '' }

    $pairs = foreach ($key in $Query.Keys | Sort-Object) {
        $value = $Query[$key]
        if ($null -eq $value) { continue }
        if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) {
            $value = @($value) -join ','
        }
        '{0}={1}' -f [uri]::EscapeDataString([string]$key), [uri]::EscapeDataString([string]$value)
    }

    @($pairs) -join '&'
}

function Get-LaunchLibraryRequestUri {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [hashtable]$Query
    )

    if ($Path -notmatch '^/2\.3\.0/') {
        throw "Path must begin with '/2.3.0/'."
    }

    $uri = '{0}{1}' -f $script:LaunchLibraryConnection.BaseUri.TrimEnd('/'), $Path
    $queryString = ConvertTo-LaunchLibraryQueryString -Query $Query
    if ($queryString) { $uri = '{0}?{1}' -f $uri, $queryString }
    $uri
}

function Get-LaunchLibraryResponseDetail {
    [CmdletBinding()]
    param(
        $Response,
        [string]$ErrorDetails
    )

    $bodies = [System.Collections.Generic.List[string]]::new()

    if ($Response) {
        try {
            if ($Response.Content) {
                $bodies.Add($Response.Content.ReadAsStringAsync().GetAwaiter().GetResult())
            } elseif ($Response.PSObject.Methods.Name -contains 'GetResponseStream') {
                $stream = $Response.GetResponseStream()
                if ($stream) {
                    $reader = [IO.StreamReader]::new($stream)
                    try {
                        $bodies.Add($reader.ReadToEnd())
                    } finally {
                        $reader.Dispose()
                        $stream.Dispose()
                    }
                }
            }
        } catch {
            Write-Verbose "Unable to read the HTTP error response body."
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($ErrorDetails)) {
        $bodies.Add($ErrorDetails)
    }

    $fallback = $null
    foreach ($body in $bodies) {
        if ([string]::IsNullOrWhiteSpace($body)) { continue }
        try {
            $errorBody = $body | ConvertFrom-Json -ErrorAction Stop
            if ($errorBody.detail) { return [string]$errorBody.detail }
        } catch {
            if (-not $fallback) { $fallback = $body.Trim() }
        }
    }

    $fallback
}

function Invoke-LaunchLibraryApiRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Uri,
        [switch]$Raw
    )

    $headers = @{ Accept = 'application/json' }
    if ($script:LaunchLibraryConnection.ApiKey) {
        $headers.Authorization = 'Token {0}' -f $script:LaunchLibraryConnection.ApiKey
    }

    try {
        $response = Invoke-RestMethod -Method Get -Uri $Uri -Headers $headers -ErrorAction Stop
    } catch {
        $statusCode = $null
        $errorResponse = $_.Exception.Response
        if ($errorResponse) { $statusCode = [int]$errorResponse.StatusCode }
        $suffix = if ($statusCode) { " (HTTP $statusCode)" } else { '' }
        $detail = Get-LaunchLibraryResponseDetail -Response $errorResponse -ErrorDetails $_.ErrorDetails.Message
        $message = if ($detail) { $detail } else { $_.Exception.Message }
        throw "Launch Library request failed${suffix}: $message"
    }

    if ($Raw) { return $response }
    if ($response.PSObject.Properties.Name -contains 'results') { return $response.results }
    $response
}

function Invoke-LaunchLibraryPagedRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Uri,
        [switch]$All,
        [switch]$Raw
    )

    $nextUri = $Uri
    do {
        $response = Invoke-LaunchLibraryApiRequest -Uri $nextUri -Raw
        if ($Raw) {
            $response
        } elseif ($response.PSObject.Properties.Name -contains 'results') {
            $response.results
        } else {
            $response
        }

        $nextUri = if ($response.PSObject.Properties.Name -contains 'next') { $response.next } else { $null }
    } while ($All -and $nextUri)
}

function Invoke-LaunchLibraryResourceCommand {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][pscustomobject]$Resource,
        [string]$Id,
        [ValidateSet('All', 'Previous', 'Upcoming')][string]$Scope = 'All',
        [hashtable]$Query,
        [switch]$All,
        [switch]$Raw
    )

    if ($Id -and -not $Resource.SupportsId) {
        throw "$($Resource.Name) does not support an ID endpoint."
    }
    if ($Scope -ne 'All' -and -not $Resource.SupportsScope) {
        throw "$($Resource.Name) does not support previous or upcoming scopes."
    }

    $path = $Resource.Path
    if ($Scope -ne 'All') { $path = '{0}{1}/' -f $path, $Scope.ToLowerInvariant() }
    if ($Id) { $path = '{0}{1}/' -f $path, [uri]::EscapeDataString($Id) }

    $uri = Get-LaunchLibraryRequestUri -Path $path -Query $Query
    Invoke-LaunchLibraryPagedRequest -Uri $uri -All:$All -Raw:$Raw
}

function Initialize-LaunchLibraryResourceCommand {
    foreach ($resource in $script:LaunchLibraryResources) {
        $command = $resource.Command
        $resourceName = $resource.Name
        $definition = @"
[CmdletBinding()]
param(
    [Parameter(Position = 0)]
    [string]`$Id,
    [ValidateSet('All', 'Previous', 'Upcoming')]
    [string]`$Scope = 'All',
    [hashtable]`$Query,
    [switch]`$All,
    [switch]`$Raw
)
Get-LaunchLibraryResource -Resource '$resourceName' -Id `$Id -Scope `$Scope -Query `$Query -All:`$All -Raw:`$Raw
"@

        Set-Item -Path "Function:script:$command" -Value ([scriptblock]::Create($definition))
    }
}