UrlQueryStringParser.psm1

function ConvertTo-UrlQueryString {
    <#
    .SYNOPSIS
        Convert the given IDictionary/hashtable into an URL query-string starting with '?'
    .OUTPUTS
        An URL query-string starting with '?'
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        # A dictionary containing key/value pairs. Use OrderedDictionary to preserve order of creation. Values will
        # be converted to strings. $null or $false values will be left out of the query string. Values equal to
        # $true will be stored as valueless entries in the query-string.
        [Parameter(ValueFromPipeline)]
        [Collections.IDictionary] $Members,

        # A previous QueryString that this is continuing. If it's non-empty, then the beginning separator will be
        # '&' instead of '?'
        [Parameter()]
        [string] $ContinuationOfString,
        
        # Leave the space-characters as space characters, which some browsers support.
        [switch] $SkipEncodeSpaces
    )
    process {
        [string] $result = "" + $ContinuationOfString
        $hasContent = $Members.Keys |
            Where-Object { Test-ValueIsWriteable $Members[$_]} |
            Foreach-Object { $true } |
            Select-Object -First 1

        if ($hasContent) {
            $result += if (-not $ContinuationOfString) {"?"}
        }
        foreach($key in $Members.Keys) {
            $key = [uri]::EscapeDataString($key.ToString())
            $foundValue = $Members[$key]
            
            # Truthy values (or, as a special-case, empty string '') are included in the dict.
            #
            # Note: -eq is NOT commutitive here, $false -eq '' but '' -ne $false. The only falsey object we want
            # is empty strings, and other forms of this code will include that.
            if (Test-ValueIsWriteable $foundValue) {
                $valueArray = @($foundValue)
                if($value -is [array]) {
                    $valueArray = $foundValue
                }
                foreach ($value in $valueArray) {
                    $value = [uri]::EscapeDataString($value.ToString())
                    if ($SkipEncodeSpaces) {
                        # only want to urlencode chars that aren't spaces in value.
                        $value = $value -replace '%20', ' '
                    }
                    if($result.Length -gt 1) {
                        $result += "&"
                    }
                    if ($value -eq $true) {
                        $result += $key
                    } else {
                        $result += "$key=$value"
                    }
                }
            }
        }
        # return
        $result
    }
}

function ConvertFrom-UrlQueryString {
    <#
    .SYNOPSIS
        Takes the given URL query string (optionally starts with "?") and converts it into a Powershell object
        (OrderedDictionary). Valueless query members (?key1&key2) will be included as $true. Empty query members
        (?key1=&key2) will be included as empty-string ''.
    #>

    [OutputType([Collections.Specialized.OrderedDictionary])]
    [CmdletBinding()]
    param (
        # URL query string (optionally starts with "?")
        [Parameter(ValueFromPipeline)]
        [string] $QueryString
    )
    process {
        $result = [ordered] @{}
        if ($QueryString) {
            if ($QueryString -like '`?*') {
                $QueryString = $QueryString.Substring(1, $QueryString.Length - 1)
            }
            $queryEntries = $QueryString -split "&"
            foreach($entry in $queryEntries) {
                if ($entry -like '*=*') {
                    $equalsCharIndex = $entry.IndexOf("=")
                    $key = $entry.Substring(0, $equalsCharIndex)
                    $key = [uri]::UnescapeDataString($key)
                    $value = $entry.Substring($equalsCharIndex + 1, $entry.Length - $equalsCharIndex - 1)
                    $value = [uri]::UnescapeDataString($value)
                    $existingValue = $result[$key]
                    if ($existingValue) {
                        # store as array (foreach flattens array)
                        $result[$key] = ($existingValue, $value | ForEach-Object {$_})
                    } else {
                        $result[$key] = $value
                    }
                } elseif ($entry) {
                    $entry = [uri]::UnescapeDataString($entry)
                    $result[$entry] = $true
                }
            }
        }

        # return
        $result
    }
}

Export-ModuleMember -Function * -Alias *

#region private functions

function Test-ValueIsWriteable {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        
        # The value to test. Can't use [string] here because that converts $null into ''
        [object] $Value 
    )
    process {
        #return
        '' -eq $Value -or $Value
    }
}

#endregion