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)
    {
        // Handle wildcard array wrapper specially
        if (currentObject is WildcardArrayWrapper)
        {
            WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject;
            return ProcessWildcardPropertyAccess(wrapper.ArrayObject, 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(String.Format("Property '{0}' does not exist", propertyName));
 
        object arrayObject = GetProperty(currentObject, propertyName);
 
        if (arrayObject == null)
            throw new InvalidOperationException(String.Format("Property '{0}' is null", propertyName));
 
        if (!IsArrayLike(arrayObject))
            throw new InvalidOperationException(String.Format("Property '{0}' is not an array", propertyName));
 
        // Handle wildcard [*] - means all elements must exist and be valid
        if (indexStr == "*")
        {
            return ProcessWildcardAccess(arrayObject, propertyName);
        }
 
        // Handle numerical index [0], [1], etc.
        int index;
        if (int.TryParse(indexStr, out index))
        {
            return ProcessNumericalAccess(arrayObject, propertyName, index);
        }
 
        throw new InvalidOperationException(String.Format("Invalid array index '{0}' for property '{1}'", indexStr, propertyName));
    }
 
    private static object ProcessWildcardAccess(object arrayObject, string propertyName)
    {
        int count = GetCount(arrayObject);
 
        if (count == 0)
            throw new InvalidOperationException(String.Format("Array '{0}' is empty - cannot validate [*]", propertyName));
 
        // 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(String.Format("Array index [{0}] is out of bounds for '{1}' (length: {2})", index, propertyName, count));
 
        // Get the specific element
        if (arrayObject is Array)
        {
            Array array = (Array)arrayObject;
            return array.GetValue(index);
        }
 
        if (arrayObject is IList)
        {
            IList list = (IList)arrayObject;
            return list[index];
        }
 
        // For IEnumerable, we need to iterate to the index
        if (arrayObject is IEnumerable)
        {
            IEnumerable enumerable = (IEnumerable)arrayObject;
            int currentIndex = 0;
            foreach (object item in enumerable)
            {
                if (currentIndex == index)
                    return item;
                currentIndex++;
            }
        }
 
        throw new InvalidOperationException(String.Format("Cannot access index [{0}] on array '{1}'", index, propertyName));
    }
 
    private static object ProcessPropertyWithValidation(object currentObject, string propertyName, char validator)
    {
        // Handle wildcard array wrapper first
        if (currentObject is WildcardArrayWrapper)
        {
            WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject;
            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(String.Format("Property '{0}' does not exist", propertyName));
 
            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(String.Format("Array '{0}' is empty", propertyName));
            }
            // 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(String.Format("Property '{0}' does not exist", propertyName));
 
            object value = GetProperty(currentObject, propertyName);
 
            if (value == null)
                throw new InvalidOperationException(String.Format("Property '{0}' is null", propertyName));
 
            if (IsEmpty(value))
                throw new InvalidOperationException(String.Format("Property '{0}' is empty or whitespace", propertyName));
 
            return value;
        }
 
        throw new InvalidOperationException(String.Format("Unknown validator '{0}'", validator));
    }
 
    private static object ProcessRegularProperty(object currentObject, string propertyName)
    {
        // Handle wildcard array wrapper - validate property exists on ALL elements
        if (currentObject is WildcardArrayWrapper)
        {
            WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject;
            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(String.Format("Property '{0}' does not exist", propertyName));
 
        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)
    {
        // First check if this is an array indexing pattern: property[index] or property[*]
        Match arrayMatch = ArrayIndexPattern.Match(propertyName);
        if (arrayMatch.Success)
        {
            string basePropertyName = arrayMatch.Groups[1].Value;
            string indexStr = arrayMatch.Groups[2].Value;
 
            // Handle array indexing after wildcard: items[0], tags[*], etc.
            if (arrayObject is Array)
            {
                Array array = (Array)arrayObject;
                for (int i = 0; i < array.Length; i++)
                {
                    object element = array.GetValue(i);
                    if (element == null)
                        throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
 
                    if (!HasProperty(element, basePropertyName))
                        throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName));
 
                    object propertyValue = GetProperty(element, basePropertyName);
                    if (propertyValue == null)
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i));
                    if (!IsArrayLike(propertyValue))
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i));
                }
 
                // All elements are valid, now handle the indexing
                object firstElement = array.GetValue(0);
                object firstPropertyValue = GetProperty(firstElement, basePropertyName);
 
                if (indexStr == "*")
                {
                    return new WildcardArrayWrapper(firstPropertyValue);
                }
                else
                {
                    int index = int.Parse(indexStr);
                    int count = GetCount(firstPropertyValue);
                    if (index < 0 || index >= count)
                        throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count));
 
                    if (firstPropertyValue is Array)
                    {
                        Array firstArray = (Array)firstPropertyValue;
                        return firstArray.GetValue(index);
                    }
                    if (firstPropertyValue is IList)
                    {
                        IList firstList = (IList)firstPropertyValue;
                        return firstList[index];
                    }
                }
            }
 
            if (arrayObject is IList)
            {
                IList list = (IList)arrayObject;
                for (int i = 0; i < list.Count; i++)
                {
                    object element = list[i];
                    if (element == null)
                        throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
                    if (!HasProperty(element, basePropertyName))
                        throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName));
 
                    object propertyValue = GetProperty(element, basePropertyName);
                    if (propertyValue == null)
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i));
                    if (!IsArrayLike(propertyValue))
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i));
                }
 
                // All elements are valid, now handle the indexing
                object firstElement = list[0];
                object firstPropertyValue = GetProperty(firstElement, basePropertyName);
 
                if (indexStr == "*")
                {
                    return new WildcardArrayWrapper(firstPropertyValue);
                }
                else
                {
                    int index = int.Parse(indexStr);
                    int count = GetCount(firstPropertyValue);
                    if (index < 0 || index >= count)
                        throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count));
 
                    if (firstPropertyValue is Array)
                    {
                        Array firstArray = (Array)firstPropertyValue;
                        return firstArray.GetValue(index);
                    }
                    if (firstPropertyValue is IList)
                    {
                        IList firstList = (IList)firstPropertyValue;
                        return firstList[index];
                    }
                }
            }
 
            throw new InvalidOperationException(String.Format("Cannot process wildcard array indexing on type {0}", arrayObject.GetType().Name));
        }
 
 
 
        // 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 array = (Array)arrayObject;
            for (int i = 0; i < array.Length; i++)
            {
                object element = array.GetValue(i);
                if (element == null)
                    throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
                if (!HasProperty(element, actualPropertyName))
                    throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, actualPropertyName));
 
                if (validator.HasValue)
                {
                    object elementValue = GetProperty(element, actualPropertyName);
                    if (elementValue == null)
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", actualPropertyName, i));
                    if (validator == '!' && IsEmpty(elementValue))
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is empty", actualPropertyName, i));
                }
            }
            return GetProperty(array.GetValue(0), actualPropertyName);
        }
 
        if (arrayObject is IList)
        {
            IList list = (IList)arrayObject;
            for (int i = 0; i < list.Count; i++)
            {
                object element = list[i];
                if (element == null)
                    throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
                if (!HasProperty(element, actualPropertyName))
                    throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, actualPropertyName));
 
                if (validator.HasValue)
                {
                    object elementValue = GetProperty(element, actualPropertyName);
                    if (elementValue == null)
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", actualPropertyName, i));
                    if (validator == '!' && IsEmpty(elementValue))
                        throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is empty", actualPropertyName, i));
                }
            }
            return GetProperty(list[0], actualPropertyName);
        }
 
        throw new InvalidOperationException(String.Format("Cannot validate wildcard array"));
    }
 
    private static void ValidatePropertyValue(object value, string propertyName, char validator, int? arrayIndex = null)
    {
        string context = arrayIndex.HasValue ? String.Format(" in array element [{0}]", arrayIndex) : "";
 
        if (validator == '?')
        {
            if (value == null)
                throw new InvalidOperationException(String.Format("Property '{0}'{1} is null", propertyName, context));
 
            if (IsArrayLike(value) && GetCount(value) == 0)
                throw new InvalidOperationException(String.Format("Array '{0}'{1} is empty", propertyName, context));
        }
        else if (validator == '!')
        {
            if (value == null)
                throw new InvalidOperationException(String.Format("Property '{0}'{1} is null", propertyName, context));
 
            if (IsEmpty(value))
                throw new InvalidOperationException(String.Format("Property '{0}'{1} is empty or whitespace", propertyName, context));
        }
    }
 
    // Helper methods (same as before)
    private static bool HasProperty(object obj, string propertyName)
    {
        if (obj == null) return false;
 
        if (obj is Hashtable)
        {
            Hashtable hashtable = (Hashtable)obj;
            return hashtable.ContainsKey(propertyName);
        }
 
        if (obj is IDictionary)
        {
            IDictionary dictionary = (IDictionary)obj;
            return dictionary.Contains(propertyName);
        }
 
        if (obj is PSObject)
        {
            PSObject psObj = (PSObject)obj;
            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 hashtable = (Hashtable)obj;
            return hashtable[propertyName];
        }
 
        if (obj is IDictionary)
        {
            IDictionary dictionary = (IDictionary)obj;
            return dictionary[propertyName];
        }
 
        if (obj is PSObject)
        {
            PSObject psObj = (PSObject)obj;
            var prop = psObj.Properties[propertyName];
            return prop != null ? prop.Value : null;
        }
 
        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 array = (Array)obj;
            return array.Length;
        }
 
        if (obj is ICollection)
        {
            ICollection collection = (ICollection)obj;
            return collection.Count;
        }
 
        if (obj is IEnumerable)
        {
            IEnumerable enumerable = (IEnumerable)obj;
            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)
        {
            string str = (string)value;
            return string.IsNullOrWhiteSpace(str);
        }
 
        if (value is Array)
        {
            Array array = (Array)value;
            return array.Length == 0;
        }
 
        if (value is ICollection)
        {
            ICollection collection = (ICollection)value;
            return collection.Count == 0;
        }
 
        return value.Equals("");
    }
}
 
// Wrapper class to handle wildcard array processing
public class WildcardArrayWrapper
{
    public object ArrayObject { get; set; }
 
    public WildcardArrayWrapper(object arrayObject)
    {
        ArrayObject = arrayObject;
    }
}
"@


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