Public/Test-Condition.ps1

function Test-Condition {
    <#
    .SYNOPSIS
    Evaluate whether a feature flag rule applies.
 
    .DESCRIPTION
    Takes the given context, properties, and rule/condition and evaluates
    whether the rule applies and what it's effect is.
 
    .PARAMETER Context
    The device context.
 
    .PARAMETER Properties
    The properties that matches given context.
 
    .PARAMETER Condition
    A condition to test which are part of rules.
 
    .EXAMPLE
    $context = Get-DeviceContext
    $properties = Read-PropertySet
    $rule = $rules[0]
    Test-Condition -Context $context -Properties $properties -Condition $rule
 
    This would return a true/false
    #>

    param (
        [Parameter(Mandatory)]
        [hashtable]
        $Context,
        [Parameter(Mandatory)]
        [PropertySet]
        [PropertySetTransformAttribute()]
        $Properties,
        [Parameter(Mandatory)]
        [Hashtable]
        $Condition
    )

    #region Recurse on groups
    if ($Condition.ContainsKey('AllOf')) {
        foreach ($child in $Condition.AllOf) {
            if (-not (Test-Condition -Context $Context -Properties $Properties -Condition $child)) {
                return $false
            }
        }
        return $true
    }

    if ($Condition.ContainsKey('AnyOf')) {
        foreach ($child in $Condition.AnyOf) {
            if (Test-Condition -Context $Context -Properties $Properties -Condition $child) {
                return $true
            }
        }
        return $false
    }

    if ($Condition.ContainsKey('Not')) {
        foreach ($child in $Condition.Not) {
            if (Test-Condition -Context $Context -Properties $Properties -Condition $child) {
                return $false
            }
        }
        return $true
    }
    #endregion Recurse on groups

    # Flat condition
    $propName = $Condition.Property
    $operator = $Condition.Operator
    $expected = $Condition.Value

    if (-not $Context.ContainsKey($propName)) {
        throw "Context is missing required property '$propName'"
    }
    if (-not $Properties.ContainsKey($propName)) {
        throw "Property metadata for '$propName' is missing"
    }

    $meta = $Properties.GetProperty($propName)
    $propType = $meta.Type
    $validation = $meta.Validation

    $actual = Convert-ToTypedValue -Type $propType -Value $Context[$propName]
    $testTypedValueSplat = @{
        PropertyDefinition = $meta
        Value = $actual
    }
    Test-TypedValue @testTypedValueSplat | Out-Null

    if (
        $operator -in @("In", "NotIn") -and
        $expected -isnot [System.Collections.IEnumerable]
    ) {
        throw 'Condition is using In/NotIn but not passing a list'
    }

    if (
        $operator -in @("In", "NotIn") -and
        $expected -is [System.Collections.IEnumerable] -and
        -not ($expected -is [string])
    ) {
        $expectedCoerced = @()
        foreach ($val in $expected) {
            $expectedCoerced += Convert-ToTypedValue -Type $propType -Value $val
        }
    } else {
        $expectedCoerced = Convert-ToTypedValue -Type $propType -Value $expected
    }

    switch ($operator) {
        "Equals" { return $actual -eq $expectedCoerced }
        "NotEquals" { return $actual -ne $expectedCoerced }
        "GreaterThan" { return $actual -gt $expectedCoerced }
        "LessThan" { return $actual -lt $expectedCoerced }
        "In" { return $expectedCoerced -contains $actual }
        "NotIn" { return -not ($expectedCoerced -contains $actual) }
        default { throw "Unsupported operator: $operator" }
    }
}