Private/ConvertTo-FilterClause.ps1

function ConvertTo-FilterClause {
    <#
    .SYNOPSIS
    Formats a single field/operator/value filter clause.

    .DESCRIPTION
    Several ISPSS services accept a filter expression as a query parameter, each in its own dialect.
    The dialects differ in operator vocabulary, in how clauses are joined, and in whether expressions
    are parenthesised, so clause assembly belongs to the calling module. What they share is how an
    individual clause is formed and how its value is quoted; this helper owns that.

    The returned clause is NOT url encoded. The filter is a value within a query string, and the
    query string is encoded as a whole further down - by Add-QueryString / ConvertTo-QueryString -
    so encoding here would double encode it.

    Values are quoted only when the value requires it: a string containing whitespace or a quote
    character, or an empty string. Quote characters within a quoted value are escaped with a
    backslash. Strings which need no quoting are emitted bare, as the dialects render them.

    Booleans are rendered lowercase and numbers in the invariant culture, so neither picks up
    PowerShell or locale specific formatting. Dates are rendered as ISO 8601 (round-trip) strings.
    A null value is rendered as an empty string.

    A single value is formatted. List operators are rendered by the calling module, which knows how
    its dialect delimits a list; an enumerable value is an error rather than a joined string.

    .PARAMETER Field
    The name of the field to filter on.

    .PARAMETER Operator
    The comparison operator. Each dialect supports its own set - validation of the operator belongs
    to the calling module, which knows which are valid for its service.

    .PARAMETER Value
    The value to compare against. Omit for operators which take no value, such as is_null.

    .PARAMETER QuoteValue
    Specify to quote the value whether or not it requires quoting.

    .PARAMETER QuoteCharacter
    The character to quote values with. Defaults to a double quote.

    .PARAMETER ValueOnly
    Specify to return the formatted value alone, without a field or operator. Dialects which render
    a list of values - for an 'in' operator, say - format each of their values this way and delimit
    them themselves.

    .EXAMPLE
    ConvertTo-FilterClause -Field name -Operator CONTAINS -Value my

    Outputs: name CONTAINS my

    .EXAMPLE
    ConvertTo-FilterClause -Field name -Operator EQ -Value 'some value'

    Outputs: name EQ "some value"

    .EXAMPLE
    ConvertTo-FilterClause -Field name -Operator EQ -Value 'my "inner" value'

    Outputs: name EQ "my \"inner\" value"

    .EXAMPLE
    ConvertTo-FilterClause -Field requestor -Operator is_null

    Outputs: requestor is_null

    .EXAMPLE
    ConvertTo-FilterClause -Value 'some value' -ValueOnly

    Outputs: "some value"

    .OUTPUTS
    String
    #>

    [CmdletBinding(DefaultParameterSetName = 'Clause')]
    [OutputType([string])]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0,
            ParameterSetName = 'Clause'
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Field,

        [parameter(
            Mandatory = $true,
            Position = 1,
            ParameterSetName = 'Clause'
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Operator,

        [parameter(
            Mandatory = $false,
            Position = 2,
            ParameterSetName = 'Clause'
        )]
        [parameter(
            Mandatory = $true,
            ParameterSetName = 'Value'
        )]
        [AllowNull()]
        [AllowEmptyString()]
        [object]$Value,

        [parameter(
            Mandatory = $true,
            ParameterSetName = 'Value'
        )]
        [switch]$ValueOnly,

        [parameter(Mandatory = $false)]
        [switch]$QuoteValue,

        [parameter(Mandatory = $false)]
        [ValidateSet('"', "'")]
        [string]$QuoteCharacter = '"'
    )

    if (-not $PSBoundParameters.ContainsKey('Value')) {

        return "$Field $Operator"

    }

    if (($Value -isnot [string]) -and ($Value -is [System.Collections.IEnumerable])) {

        throw 'ConvertTo-FilterClause formats a single value. Compose clauses for list operators in the calling module, which knows how its dialect renders a list'

    }

    if ($Value -is [bool]) {

        $FormattedValue = $Value.ToString().ToLowerInvariant()
        $RequiresQuoting = $false

    } elseif ($Value -is [datetime]) {

        $FormattedValue = $Value.ToString('o', [cultureinfo]::InvariantCulture)
        $RequiresQuoting = $false

    } elseif ($Value -is [valuetype]) {

        $FormattedValue = [string]::Format([cultureinfo]::InvariantCulture, '{0}', $Value)
        $RequiresQuoting = $false

    } else {

        $FormattedValue = [string]$Value
        $RequiresQuoting = ($FormattedValue -match '\s') -or
            ($FormattedValue.Contains($QuoteCharacter)) -or
            ([string]::IsNullOrEmpty($FormattedValue))

    }

    if ($QuoteValue -or $RequiresQuoting) {

        #A quote character within a quoted value is escaped with a backslash
        $FormattedValue = $FormattedValue.Replace($QuoteCharacter, "\$QuoteCharacter")
        $FormattedValue = "$QuoteCharacter$FormattedValue$QuoteCharacter"

    }

    if ($ValueOnly) {

        return $FormattedValue

    }

    "$Field $Operator $FormattedValue"

}