Private/PSFluentObjectValidation.ps1

$PSFluentObjectValidation = @"
using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation;
using System.Text.RegularExpressions;
 
public static class PSFluentObjectValidation
{
    private static readonly Regex PropertyWithValidation = new Regex(@"^(.+)([?!])$", RegexOptions.Compiled);
    private static readonly Regex ArrayIndexPattern = new Regex(@"^(.+)\[(\*|\d+)\]$", RegexOptions.Compiled);
 
    public static bool TestExists(object inputObject, string key)
    {
        try
        {
            AssertExists(inputObject, key);
            return true;
        }
        catch
        {
            return false;
        }
    }
 
    public static void AssertExists(object inputObject, string key)
    {
        if (inputObject == null)
            throw new ArgumentException("InputObject cannot be null");
 
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("Key cannot be null or empty");
 
        string[] keyParts = key.Split('.');
        object currentObject = inputObject;
 
        foreach (string part in keyParts)
        {
            currentObject = ProcessKeyPart(currentObject, part);
        }
    }
 
    private static object ProcessKeyPart(object currentObject, string part)
    {
        // Check for array indexing: property[index] or property[*]
        Match arrayMatch = ArrayIndexPattern.Match(part);
        if (arrayMatch.Success)
        {
            string propertyName = arrayMatch.Groups[1].Value;
            string indexStr = arrayMatch.Groups[2].Value;
 
            return ProcessArrayAccess(currentObject, propertyName, indexStr);
        }
 
        // Check for validation suffixes: property? or property!
        Match validationMatch = PropertyWithValidation.Match(part);
        if (validationMatch.Success)
        {
            string propertyName = validationMatch.Groups[1].Value;
            char validator = validationMatch.Groups[2].Value[0];
 
            return ProcessPropertyWithValidation(currentObject, propertyName, validator);
        }
 
        // Regular property navigation
        return ProcessRegularProperty(currentObject, part);
    }
 
    private static object ProcessArrayAccess(object currentObject, string propertyName, string indexStr)
    {
        // First get the property (should be an array)
        if (!HasProperty(currentObject, propertyName))
            throw new InvalidOperationException($"Property '{propertyName}' does not exist");
 
        object arrayObject = GetProperty(currentObject, propertyName);
 
        if (arrayObject == null)
            throw new InvalidOperationException($"Property '{propertyName}' is null");
 
        if (!IsArrayLike(arrayObject))
            throw new InvalidOperationException($"Property '{propertyName}' is not an array");
 
        // Handle wildcard [*] - means all elements must exist and be valid
        if (indexStr == "*")
        {
            return ProcessWildcardAccess(arrayObject, propertyName);
        }
 
        // Handle numerical index [0], [1], etc.
        if (int.TryParse(indexStr, out int index))
        {
            return ProcessNumericalAccess(arrayObject, propertyName, index);
        }
 
        throw new InvalidOperationException($"Invalid array index '{indexStr}' for property '{propertyName}'");
    }
 
    private static object ProcessWildcardAccess(object arrayObject, string propertyName)
    {
        int count = GetCount(arrayObject);
 
        if (count == 0)
            throw new InvalidOperationException($"Array '{propertyName}' is empty - cannot validate [*]");
 
        // For wildcard, we return the array object itself
        // The next part in the chain will validate against all elements
        return new WildcardArrayWrapper(arrayObject);
    }
 
    private static object ProcessNumericalAccess(object arrayObject, string propertyName, int index)
    {
        int count = GetCount(arrayObject);
 
        if (index < 0 || index >= count)
            throw new InvalidOperationException($"Array index [{index}] is out of bounds for '{propertyName}' (length: {count})");
 
        // Get the specific element
        if (arrayObject is Array array)
            return array.GetValue(index);
 
        if (arrayObject is IList list)
            return list[index];
 
        // For IEnumerable, we need to iterate to the index
        if (arrayObject is IEnumerable enumerable)
        {
            int currentIndex = 0;
            foreach (object item in enumerable)
            {
                if (currentIndex == index)
                    return item;
                currentIndex++;
            }
        }
 
        throw new InvalidOperationException($"Cannot access index [{index}] on array '{propertyName}'");
    }
 
    private static object ProcessPropertyWithValidation(object currentObject, string propertyName, char validator)
    {
        // Handle wildcard array wrapper first
        if (currentObject is WildcardArrayWrapper wrapper)
        {
            return ProcessWildcardPropertyAccess(wrapper.ArrayObject, propertyName + validator);
        }
 
        if (validator == '?')
        {
            // Handle object/array validation: key? - ONLY check existence, allow null values
            if (!HasProperty(currentObject, propertyName))
                throw new InvalidOperationException($"Property '{propertyName}' does not exist");
 
            object value = GetProperty(currentObject, propertyName);
 
            // For ? operator, we only care about existence, not null values
            // If it's an array-like object, check it's not empty, but allow null values otherwise
            if (value != null && IsArrayLike(value))
            {
                // For arrays, check that it's not empty
                if (GetCount(value) == 0)
                    throw new InvalidOperationException($"Array '{propertyName}' is empty");
            }
            // Note: We don't check IsObjectLike here because ? only validates existence
 
            return value; // Can be null - that's valid for ? operator
        }
        else if (validator == '!')
        {
            // Validate non-empty: key!
            if (!HasProperty(currentObject, propertyName))
                throw new InvalidOperationException($"Property '{propertyName}' does not exist");
 
            object value = GetProperty(currentObject, propertyName);
 
            if (value == null)
                throw new InvalidOperationException($"Property '{propertyName}' is null");
 
            if (IsEmpty(value))
                throw new InvalidOperationException($"Property '{propertyName}' is empty or whitespace");
 
            return value;
        }
 
        throw new InvalidOperationException($"Unknown validator '{validator}'");
    }
 
    private static object ProcessRegularProperty(object currentObject, string propertyName)
    {
        // Handle wildcard array wrapper - validate property exists on ALL elements
        if (currentObject is WildcardArrayWrapper wrapper)
        {
            return ProcessWildcardPropertyAccess(wrapper.ArrayObject, propertyName);
        }
 
        // Check for validation suffixes even in regular properties
        Match validationMatch = PropertyWithValidation.Match(propertyName);
        if (validationMatch.Success)
        {
            string actualPropertyName = validationMatch.Groups[1].Value;
            char validator = validationMatch.Groups[2].Value[0];
 
            return ProcessPropertyWithValidation(currentObject, actualPropertyName, validator);
        }
 
        // Regular property navigation - allow null values (only ! operator should reject nulls)
        if (!HasProperty(currentObject, propertyName))
            throw new InvalidOperationException($"Property '{propertyName}' does not exist");
 
        object value = GetProperty(currentObject, propertyName);
 
        // For regular navigation, null values are allowed (property exists but is null)
        // Only the ! operator should reject null/empty values
        return value;
    }
 
    private static object ProcessWildcardPropertyAccess(object arrayObject, string propertyName)
    {
        // Parse validation suffix if present
        Match validationMatch = PropertyWithValidation.Match(propertyName);
        string actualPropertyName = propertyName;
        char? validator = null;
 
        if (validationMatch.Success)
        {
            actualPropertyName = validationMatch.Groups[1].Value;
            validator = validationMatch.Groups[2].Value[0];
        }
 
        // Validate ALL elements have this property
        if (arrayObject is Array array)
        {
            for (int i = 0; i < array.Length; i++)
            {
                object element = array.GetValue(i);
                if (element == null)
                    throw new InvalidOperationException($"Array element [{i}] is null");
                if (!HasProperty(element, actualPropertyName))
                    throw new InvalidOperationException($"Array element [{i}] does not have property '{actualPropertyName}'");
 
                if (validator.HasValue)
                {
                    object elementValue = GetProperty(element, actualPropertyName);
                    if (elementValue == null)
                        throw new InvalidOperationException($"Property '{actualPropertyName}' in element [{i}] is null");
                    if (validator == '!' && IsEmpty(elementValue))
                        throw new InvalidOperationException($"Property '{actualPropertyName}' in element [{i}] is empty");
                }
            }
            return GetProperty(array.GetValue(0), actualPropertyName);
        }
 
        if (arrayObject is IList list)
        {
            for (int i = 0; i < list.Count; i++)
            {
                object element = list[i];
                if (element == null)
                    throw new InvalidOperationException($"Array element [{i}] is null");
                if (!HasProperty(element, actualPropertyName))
                    throw new InvalidOperationException($"Array element [{i}] does not have property '{actualPropertyName}'");
 
                if (validator.HasValue)
                {
                    object elementValue = GetProperty(element, actualPropertyName);
                    if (elementValue == null)
                        throw new InvalidOperationException($"Property '{actualPropertyName}' in element [{i}] is null");
                    if (validator == '!' && IsEmpty(elementValue))
                        throw new InvalidOperationException($"Property '{actualPropertyName}' in element [{i}] is empty");
                }
            }
            return GetProperty(list[0], actualPropertyName);
        }
 
        throw new InvalidOperationException($"Cannot validate wildcard array");
    }
 
    private static void ValidatePropertyValue(object value, string propertyName, char validator, int? arrayIndex = null)
    {
        string context = arrayIndex.HasValue ? $" in array element [{arrayIndex}]" : "";
 
        if (validator == '?')
        {
            if (value == null)
                throw new InvalidOperationException($"Property '{propertyName}'{context} is null");
 
            if (IsArrayLike(value) && GetCount(value) == 0)
                throw new InvalidOperationException($"Array '{propertyName}'{context} is empty");
        }
        else if (validator == '!')
        {
            if (value == null)
                throw new InvalidOperationException($"Property '{propertyName}'{context} is null");
 
            if (IsEmpty(value))
                throw new InvalidOperationException($"Property '{propertyName}'{context} is empty or whitespace");
        }
    }
 
    // Helper methods (same as before)
    private static bool HasProperty(object obj, string propertyName)
    {
        if (obj == null) return false;
 
        if (obj is Hashtable hashtable)
            return hashtable.ContainsKey(propertyName);
 
        if (obj is IDictionary dictionary)
            return dictionary.Contains(propertyName);
 
        if (obj is PSObject psObj)
            return psObj.Properties[propertyName] != null;
 
        var type = obj.GetType();
        return type.GetProperty(propertyName) != null || type.GetField(propertyName) != null;
    }
 
    private static object GetProperty(object obj, string propertyName)
    {
        if (obj == null) return null;
 
        if (obj is Hashtable hashtable)
            return hashtable[propertyName];
 
        if (obj is IDictionary dictionary)
            return dictionary[propertyName];
 
        if (obj is PSObject psObj)
        {
            var prop = psObj.Properties[propertyName];
            return prop?.Value;
        }
 
        var type = obj.GetType();
        var property = type.GetProperty(propertyName);
        if (property != null)
            return property.GetValue(obj);
 
        var field = type.GetField(propertyName);
        if (field != null)
            return field.GetValue(obj);
 
        return null;
    }
 
    private static bool IsObjectLike(object obj)
    {
        if (obj == null) return false;
        return obj is Hashtable || obj is IDictionary || obj is PSObject ||
               (!IsArrayLike(obj) && !(obj is string));
    }
 
    private static bool IsArrayLike(object obj)
    {
        if (obj == null || obj is string) return false;
        return obj is Array || obj is IList || obj is IEnumerable;
    }
 
    private static int GetCount(object obj)
    {
        if (obj == null) return 0;
 
        if (obj is Array array)
            return array.Length;
 
        if (obj is ICollection collection)
            return collection.Count;
 
        if (obj is IEnumerable enumerable)
        {
            int count = 0;
            foreach (var item in enumerable)
                count++;
            return count;
        }
 
        return 1;
    }
 
    private static bool IsEmpty(object value)
    {
        if (value == null) return true;
 
        if (value is string str)
            return string.IsNullOrWhiteSpace(str);
 
        if (value is Array array)
            return array.Length == 0;
 
        if (value is ICollection collection)
            return collection.Count == 0;
 
        return value.Equals("");
    }
}
 
// Wrapper class to handle wildcard array processing
public class WildcardArrayWrapper
{
    public object ArrayObject { get; }
 
    public WildcardArrayWrapper(object arrayObject)
    {
        ArrayObject = arrayObject;
    }
}
"@


try {
    Add-Type -TypeDefinition $PSFluentObjectValidation -ReferencedAssemblies @(
        'System.Core',
        'System.Management.Automation',
        'System.Text.RegularExpressions'
    ) -Language CSharp
} catch {
    throw
}