< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
86%
Covered lines: 63
Uncovered lines: 10
Coverable lines: 73
Total lines: 195
Line coverage: 86.3%
Branch coverage
62%
Covered branches: 56
Total branches: 89
Branch coverage: 62.9%
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@c33ec02a85e4f8d6061aeaab5a5e8c3a8b665594 10/15/2025 - 21:27:26 Line coverage: 86.3% (63/73) Branch coverage: 62.9% (56/89) Total lines: 195 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b665594

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Sanitize(...)100%11100%
Sanitize(...)85.71%141486.66%
IsSimpleScalar(...)100%66100%
TryHandlePowerShellObject(...)87.5%8883.33%
TryHandleDictionary(...)75%88100%
TryHandleEnumerable(...)100%66100%
TryHandleFriendlyTypes(...)30%201053.84%
TryAddVisited(...)50%44100%
IsPSCustomObject(...)50%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)
 521        => 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    {
 1032        if (value is null)
 33        {
 034            return null;
 35        }
 36
 37        // Fast-path for scalars and special normalization
 1038        if (IsSimpleScalar(value, out var simple))
 39        {
 540            return simple;
 41        }
 42
 43        // Prevent cycles for reference types
 544        if (!TryAddVisited(value, visited))
 45        {
 146            return "[Circular]";
 47        }
 48
 49        // Handlers for common composite/PowerShell shapes
 450        if (TryHandlePowerShellObject(value, visited, depth, out var psResult))
 51        {
 152            return psResult;
 53        }
 354        if (TryHandleDictionary(value, visited, depth, out var dictResult))
 55        {
 156            return dictResult;
 57        }
 258        if (TryHandleEnumerable(value, visited, depth, out var listResult))
 59        {
 160            return listResult;
 61        }
 62
 63        // Friendly representations for tricky types
 164        if (TryHandleFriendlyTypes(value, out var friendly))
 65        {
 166            return friendly;
 67        }
 68
 69        // Fallback: let System.Text.Json serialize public properties as-is
 070        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
 1076        if (value is string)
 77        {
 178            normalized = value;
 179            return true;
 80        }
 81
 982        var type = value.GetType();
 983        if (type.IsValueType)
 84        {
 485            if (value is TimeSpan ts)
 86            {
 287                normalized = ts.ToString("c", CultureInfo.InvariantCulture);
 288                return true;
 89            }
 90
 291            normalized = value;
 292            return true;
 93        }
 94
 595        normalized = null;
 596        return false;
 97    }
 98
 99    private static bool TryHandlePowerShellObject(object value, HashSet<object> visited, int depth, out object? result)
 100    {
 4101        if (value is PSObject pso)
 102        {
 1103            if (IsPSCustomObject(pso))
 104            {
 1105                var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 6106                foreach (var prop in pso.Properties)
 107                {
 2108                    if (IsSpecialMember(prop.Name))
 109                    {
 110                        continue; // Skip meta/special members that cause cycles
 111                    }
 2112                    dict[prop.Name] = Sanitize(prop.Value, visited, depth + 1);
 113                }
 1114                result = dict;
 1115                return true;
 116            }
 117
 118            // Otherwise unwrap to base object and continue
 0119            result = Sanitize(pso.BaseObject, visited, depth + 1);
 0120            return true;
 121        }
 122
 3123        result = null;
 3124        return false;
 125    }
 126
 127    private static bool TryHandleDictionary(object value, HashSet<object> visited, int depth, out object? result)
 128    {
 3129        if (value is IDictionary idict)
 130        {
 1131            var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 4132            foreach (DictionaryEntry entry in idict)
 133            {
 1134                var key = entry.Key?.ToString() ?? string.Empty;
 1135                dict[key] = Sanitize(entry.Value, visited, depth + 1);
 136            }
 1137            result = dict;
 1138            return true;
 139        }
 140
 2141        result = null;
 2142        return false;
 143    }
 144
 145    private static bool TryHandleEnumerable(object value, HashSet<object> visited, int depth, out object? result)
 146    {
 2147        if (value is IEnumerable enumerable and not string)
 148        {
 1149            var list = new List<object?>();
 6150            foreach (var item in enumerable)
 151            {
 2152                list.Add(Sanitize(item, visited, depth + 1));
 153            }
 1154            result = list;
 1155            return true;
 156        }
 157
 1158        result = null;
 1159        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:
 0181                result = null;
 0182                return false;
 183        }
 184    }
 185
 186    private static bool TryAddVisited(object o, HashSet<object> visited) =>
 187        // Strings and value types are safe (immutable/value)
 5188        o is string || o.GetType().IsValueType || visited.Add(o);
 189
 190    private static bool IsPSCustomObject(PSObject pso)
 1191        => pso.BaseObject is PSCustomObject || pso.TypeNames.Contains("System.Management.Automation.PSCustomObject");
 192
 193    private static bool IsSpecialMember(string name)
 2194        => name is "PSObject" or "BaseObject" or "Members" or "ImmediateBaseObject" or "Properties" or "TypeNames" or "M
 195}