Private/Uri.ps1

function ConvertTo-DattoApiPathSegment {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowEmptyString()]
        [string]$Value
    )
    process {
        [uri]::EscapeDataString($Value)
    }
}

function ConvertTo-DattoApiInvariantString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [object]$Value
    )

    if ($Value -is [bool]) {
        return $Value.ToString().ToLowerInvariant()
    }
    if ($Value -is [datetimeoffset]) {
        return $Value.ToString('o', [Globalization.CultureInfo]::InvariantCulture)
    }
    if ($Value -is [datetime]) {
        return $Value.ToString('o', [Globalization.CultureInfo]::InvariantCulture)
    }
    if ($Value -is [System.IFormattable]) {
        return $Value.ToString($null, [Globalization.CultureInfo]::InvariantCulture)
    }
    return [string]$Value
}

function ConvertTo-DattoApiQueryString {
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Collections.IDictionary]$Query
    )

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

    $pairs = [System.Collections.Generic.List[string]]::new()
    foreach ($key in @($Query.Keys | Sort-Object)) {
        $value = $Query[$key]
        if ($null -eq $value) { continue }

        if (($value -is [System.Collections.IEnumerable]) -and -not ($value -is [string]) -and -not ($value -is [System.Collections.IDictionary])) {
            $parts = [System.Collections.Generic.List[string]]::new()
            foreach ($part in $value) {
                if ($null -ne $part) {
                    $parts.Add((ConvertTo-DattoApiInvariantString -Value $part))
                }
            }
            $valueText = $parts -join ','
        }
        else {
            $valueText = ConvertTo-DattoApiInvariantString -Value $value
        }

        $encodedKey = [uri]::EscapeDataString([string]$key)
        $encodedValue = [uri]::EscapeDataString($valueText)
        $pairs.Add(('{0}={1}' -f $encodedKey, $encodedValue))
    }
    return ($pairs -join '&')
}

function Copy-DattoApiQuery {
    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Collections.IDictionary]$Query
    )

    $copy = @{}
    if ($null -ne $Query) {
        foreach ($key in $Query.Keys) {
            $copy[$key] = $Query[$key]
        }
    }
    return $copy
}

function New-DattoApiRequestUri {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$ConnectionState,

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

        [Parameter()]
        [AllowNull()]
        [System.Collections.IDictionary]$Query
    )

    if (-not $Path.StartsWith('/v1/', [StringComparison]::OrdinalIgnoreCase)) {
        throw [System.ArgumentException]::new("Datto API paths must begin with '/v1/'. Received '$Path'.")
    }
    if ($Path.StartsWith('//') -or $Path.Contains('://') -or $Path.Contains('?') -or $Path.Contains('#') -or $Path.Contains('\')) {
        throw [System.ArgumentException]::new('The API path must be a relative Datto v1 path without a host, query string, fragment, or backslash.')
    }
    foreach ($character in $Path.ToCharArray()) {
        if ([char]::IsControl($character)) {
            throw [System.ArgumentException]::new('The API path contains a control character.')
        }
    }
    $decodedPath = [uri]::UnescapeDataString($Path)
    foreach ($character in $decodedPath.ToCharArray()) {
        if ([char]::IsControl($character)) {
            throw [System.ArgumentException]::new('The decoded API path contains a control character.')
        }
    }
    foreach ($segment in $decodedPath.Split('/')) {
        if ($segment -eq '.' -or $segment -eq '..') {
            throw [System.ArgumentException]::new('The API path cannot contain dot or parent-directory segments.')
        }
    }

    $baseUri = [uri]$ConnectionState.BaseUri
    $uriText = $baseUri.AbsoluteUri.TrimEnd('/') + $Path
    $queryString = ConvertTo-DattoApiQueryString -Query $Query
    if (-not [string]::IsNullOrWhiteSpace($queryString)) {
        $uriText += '?' + $queryString
    }
    return [uri]$uriText
}