< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Utilities.Json.PayloadSanitizer
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/Json/PayloadSanitizer.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
94%
Covered lines: 69
Uncovered lines: 4
Coverable lines: 73
Total lines: 195
Line coverage: 94.5%
Branch coverage
68%
Covered branches: 61
Total branches: 89
Branch coverage: 68.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/15/2025 - 21:27:26 Line coverage: 86.3% (63/73) Branch coverage: 62.9% (56/89) Total lines: 195 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559412/15/2025 - 18:44:50 Line coverage: 91.7% (67/73) Branch coverage: 66.2% (59/89) Total lines: 195 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61801/15/2026 - 23:50:39 Line coverage: 94.5% (69/73) Branch coverage: 68.5% (61/89) Total lines: 195 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa 10/15/2025 - 21:27:26 Line coverage: 86.3% (63/73) Branch coverage: 62.9% (56/89) Total lines: 195 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559412/15/2025 - 18:44:50 Line coverage: 91.7% (67/73) Branch coverage: 66.2% (59/89) Total lines: 195 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61801/15/2026 - 23:50:39 Line coverage: 94.5% (69/73) Branch coverage: 68.5% (61/89) Total lines: 195 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Sanitize(...)100%11100%
Sanitize(...)100%1414100%
IsSimpleScalar(...)100%66100%
TryHandlePowerShellObject(...)100%88100%
TryHandleDictionary(...)75%88100%
TryHandleEnumerable(...)100%66100%
TryHandleFriendlyTypes(...)40%131069.23%
TryAddVisited(...)50%44100%
IsPSCustomObject(...)100%22100%
IsSpecialMember(...)41.93%3131100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/Json/PayloadSanitizer.cs

#LineLine coverage
 1using System.Collections;
 2using System.Management.Automation;
 3using System.Globalization;
 4
 5namespace Kestrun.Utilities.Json;
 6
 7/// <summary>
 8/// Utilities to sanitize arbitrary payloads (especially PowerShell objects) into JSON-friendly shapes
 9/// for System.Text.Json/SignalR serialization without reference cycles.
 10/// </summary>
 11public static class PayloadSanitizer
 12{
 13    /// <summary>
 14    /// Returns a sanitized version of the provided value suitable for JSON serialization.
 15    /// - Unwraps PSObject/PSCustomObject into dictionaries
 16    /// - Converts IDictionary into Dictionary&lt;string, object?&gt;
 17    /// - Converts IEnumerable into List&lt;object?&gt;
 18    /// - Replaces circular references with the string "[Circular]"
 19    /// </summary>
 20    public static object? Sanitize(object? value)
 3921        => Sanitize(value, new HashSet<object>(ReferenceEqualityComparer.Instance), 0);
 22
 23    /// <summary>
 24    /// Internal recursive sanitizer with cycle detection.
 25    /// </summary>
 26    /// <param name="value">The value to sanitize.</param>
 27    /// <param name="visited">The set of visited objects.</param>
 28    /// <param name="depth">The current depth in the object graph.</param>
 29    /// <returns>The sanitized value.</returns>
 30    private static object? Sanitize(object? value, HashSet<object> visited, int depth)
 31    {
 6032        if (value is null)
 33        {
 234            return null;
 35        }
 36
 37        // Fast-path for scalars and special normalization
 5838        if (IsSimpleScalar(value, out var simple))
 39        {
 2640            return simple;
 41        }
 42
 43        // Prevent cycles for reference types
 3244        if (!TryAddVisited(value, visited))
 45        {
 146            return "[Circular]";
 47        }
 48
 49        // Handlers for common composite/PowerShell shapes
 3150        if (TryHandlePowerShellObject(value, visited, depth, out var psResult))
 51        {
 552            return psResult;
 53        }
 2654        if (TryHandleDictionary(value, visited, depth, out var dictResult))
 55        {
 356            return dictResult;
 57        }
 2358        if (TryHandleEnumerable(value, visited, depth, out var listResult))
 59        {
 360            return listResult;
 61        }
 62
 63        // Friendly representations for tricky types
 2064        if (TryHandleFriendlyTypes(value, out var friendly))
 65        {
 166            return friendly;
 67        }
 68
 69        // Fallback: let System.Text.Json serialize public properties as-is
 1970        return value;
 71    }
 72
 73    private static bool IsSimpleScalar(object value, out object? normalized)
 74    {
 75        // Treat value types and strings as simple scalars, with a special case for TimeSpan
 5876        if (value is string)
 77        {
 1978            normalized = value;
 1979            return true;
 80        }
 81
 3982        var type = value.GetType();
 3983        if (type.IsValueType)
 84        {
 785            if (value is TimeSpan ts)
 86            {
 287                normalized = ts.ToString("c", CultureInfo.InvariantCulture);
 288                return true;
 89            }
 90
 591            normalized = value;
 592            return true;
 93        }
 94
 3295        normalized = null;
 3296        return false;
 97    }
 98
 99    private static bool TryHandlePowerShellObject(object value, HashSet<object> visited, int depth, out object? result)
 100    {
 31101        if (value is PSObject pso)
 102        {
 5103            if (IsPSCustomObject(pso))
 104            {
 2105                var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 12106                foreach (var prop in pso.Properties)
 107                {
 4108                    if (IsSpecialMember(prop.Name))
 109                    {
 110                        continue; // Skip meta/special members that cause cycles
 111                    }
 4112                    dict[prop.Name] = Sanitize(prop.Value, visited, depth + 1);
 113                }
 2114                result = dict;
 2115                return true;
 116            }
 117
 118            // Otherwise unwrap to base object and continue
 3119            result = Sanitize(pso.BaseObject, visited, depth + 1);
 3120            return true;
 121        }
 122
 26123        result = null;
 26124        return false;
 125    }
 126
 127    private static bool TryHandleDictionary(object value, HashSet<object> visited, int depth, out object? result)
 128    {
 26129        if (value is IDictionary idict)
 130        {
 3131            var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 24132            foreach (DictionaryEntry entry in idict)
 133            {
 9134                var key = entry.Key?.ToString() ?? string.Empty;
 9135                dict[key] = Sanitize(entry.Value, visited, depth + 1);
 136            }
 3137            result = dict;
 3138            return true;
 139        }
 140
 23141        result = null;
 23142        return false;
 143    }
 144
 145    private static bool TryHandleEnumerable(object value, HashSet<object> visited, int depth, out object? result)
 146    {
 23147        if (value is IEnumerable enumerable and not string)
 148        {
 3149            var list = new List<object?>();
 16150            foreach (var item in enumerable)
 151            {
 5152                list.Add(Sanitize(item, visited, depth + 1));
 153            }
 3154            result = list;
 3155            return true;
 156        }
 157
 20158        result = null;
 20159        return false;
 160    }
 161
 162    private static bool TryHandleFriendlyTypes(object value, out object? result)
 163    {
 164        switch (value)
 165        {
 166            case Type t:
 0167                result = t.FullName;
 0168                return true;
 169            case Delegate del:
 0170                result = del.Method?.Name ?? del.GetType().Name;
 0171                return true;
 172            case Exception ex:
 1173                result = new
 1174                {
 1175                    Type = ex.GetType().FullName,
 1176                    ex.Message,
 1177                    ex.StackTrace
 1178                };
 1179                return true;
 180            default:
 19181                result = null;
 19182                return false;
 183        }
 184    }
 185
 186    private static bool TryAddVisited(object o, HashSet<object> visited) =>
 187        // Strings and value types are safe (immutable/value)
 32188        o is string || o.GetType().IsValueType || visited.Add(o);
 189
 190    private static bool IsPSCustomObject(PSObject pso)
 5191        => pso.BaseObject is PSCustomObject || pso.TypeNames.Contains("System.Management.Automation.PSCustomObject");
 192
 193    private static bool IsSpecialMember(string name)
 4194        => name is "PSObject" or "BaseObject" or "Members" or "ImmediateBaseObject" or "Properties" or "TypeNames" or "M
 195}