< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.OpenApi.OpenApiJsonNodeFactory
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiJsonNodeFactory.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
67%
Covered lines: 66
Uncovered lines: 32
Coverable lines: 98
Total lines: 275
Line coverage: 67.3%
Branch coverage
59%
Covered branches: 61
Total branches: 102
Branch coverage: 59.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/12/2025 - 17:27:19 Line coverage: 0% (0/36) Branch coverage: 0% (0/64) Total lines: 88 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/08/2026 - 02:20:28 Line coverage: 16.6% (6/36) Branch coverage: 25% (16/64) Total lines: 88 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/09/2026 - 06:56:42 Line coverage: 52.7% (19/36) Branch coverage: 60.9% (39/64) Total lines: 88 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/11/2026 - 19:55:44 Line coverage: 51% (50/98) Branch coverage: 45% (46/102) Total lines: 275 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/14/2026 - 07:55:07 Line coverage: 59.1% (58/98) Branch coverage: 52.9% (54/102) Total lines: 275 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 18:18:02 Line coverage: 67.3% (66/98) Branch coverage: 59.8% (61/102) Total lines: 275 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c018 12/12/2025 - 17:27:19 Line coverage: 0% (0/36) Branch coverage: 0% (0/64) Total lines: 88 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/08/2026 - 02:20:28 Line coverage: 16.6% (6/36) Branch coverage: 25% (16/64) Total lines: 88 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/09/2026 - 06:56:42 Line coverage: 52.7% (19/36) Branch coverage: 60.9% (39/64) Total lines: 88 Tag: Kestrun/Kestrun@94f8107dc592fa7eaec45c0dd5f9fffbd41bc14501/11/2026 - 19:55:44 Line coverage: 51% (50/98) Branch coverage: 45% (46/102) Total lines: 275 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/14/2026 - 07:55:07 Line coverage: 59.1% (58/98) Branch coverage: 52.9% (54/102) Total lines: 275 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/17/2026 - 18:18:02 Line coverage: 67.3% (66/98) Branch coverage: 59.8% (61/102) Total lines: 275 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c018

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ToNode(...)62%1825062.5%
Unwrap(...)37.5%10866.66%
ToJsonObject(...)100%66100%
ToJsonArray(...)50%2275%
FromPocoOrString(...)50%121075%
TryConvertPsObjectProperties(...)12.5%35825%
ShouldFallbackToString(...)50%44100%
TryConvertPublicProperties(...)91.66%121292.3%
IsReadableNonIndexer(...)50%22100%
TryGetPropertyValue(...)100%1166.66%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/OpenApi/OpenApiJsonNodeFactory.cs

#LineLine coverage
 1using System.Collections;
 2using System.Management.Automation;
 3using System.Reflection;
 4using System.Text.Json.Nodes;
 5
 6namespace Kestrun.OpenApi;
 7/// <summary>
 8/// Helpers to create System.Text.Json.Nodes from .NET objects for OpenAPI representation.
 9/// </summary>
 10public static class OpenApiJsonNodeFactory
 11{
 12    /// <summary>
 13    /// Create a JsonNode from a .NET object.
 14    /// </summary>
 15    /// <param name="value">The .NET object to convert.</param>
 16    /// <returns>A JsonNode representation of the object.</returns>
 17    public static JsonNode? ToNode(object? value)
 18    {
 10219        value = Unwrap(value);
 11620        if (value is null) { return null; }
 21
 22        // Handle various common types
 8823        return value switch
 8824        {
 8825            // primitives
 126            bool b => JsonValue.Create(b),
 5027            string s => JsonValue.Create(s),
 8828
 8829            // integers (preserve range; avoid ulong->long overflow)
 030            sbyte sb => JsonValue.Create((long)sb),
 031            byte by => JsonValue.Create((long)by),
 032            short sh => JsonValue.Create((long)sh),
 033            ushort ush => JsonValue.Create((long)ush),
 1334            int i => JsonValue.Create((long)i),
 035            uint ui => JsonValue.Create((ulong)ui <= long.MaxValue ? ui : (decimal)ui),
 036            long l => JsonValue.Create(l),
 037            ulong ul => ul <= long.MaxValue ? JsonValue.Create((long)ul) : JsonValue.Create((decimal)ul),
 8838
 8839            // floating/decimal
 040            float f => JsonValue.Create((double)f),
 041            double d => JsonValue.Create(d),
 042            decimal m => JsonValue.Create(m),
 8843
 8844            // common .NET types
 145            DateTime dt => JsonValue.Create(dt.ToString("o")),
 046            DateTimeOffset dto => JsonValue.Create(dto.ToString("o")),
 047            TimeSpan ts => JsonValue.Create(ts.ToString("c")),
 048            Guid g => JsonValue.Create(g.ToString()),
 049            Uri uri => JsonValue.Create(uri.ToString()),
 8850
 8851            // enums -> string (usually nicer for OpenAPI-ish metadata)
 052            Enum e => JsonValue.Create(e.ToString()),
 8853
 8854            // dictionaries / lists
 355            IDictionary dict => ToJsonObject(dict),
 1656            IEnumerable en when value is not string => ToJsonArray(en),
 8857
 8858            // fallback
 1259            _ => FromPocoOrString(value),
 8860        };
 61    }
 62
 63    /// <summary>
 64    /// Unwraps common wrapper types to get the underlying value.
 65    /// </summary>
 66    /// <param name="value">The object to unwrap.</param>
 67    /// <returns>The unwrapped object, or the original if no unwrapping was needed.</returns>
 68    private static object? Unwrap(object? value)
 69    {
 10270        if (value is null)
 71        {
 1472            return null;
 73        }
 74        // PowerShell wraps lots of values in PSObject
 8875        if (value is PSObject pso)
 76        {
 77            // If it's a PSCustomObject / has note properties, keep PSObject itself
 78            // so we can serialize its Properties cleanly.
 79            // Otherwise unwrap to BaseObject.
 080            return pso.BaseObject is not null && pso.BaseObject.GetType() != typeof(PSCustomObject) ?
 081                pso.BaseObject : pso;
 82        }
 83
 8884        return value;
 85    }
 86
 87    /// <summary>
 88    /// Converts an IDictionary to a JsonObject.
 89    /// </summary>
 90    /// <param name="dict">The dictionary to convert.</param>
 91    /// <returns>A JsonObject representing the dictionary.</returns>
 92    private static JsonObject ToJsonObject(IDictionary dict)
 93    {
 394        var obj = new JsonObject();
 95
 1896        foreach (DictionaryEntry de in dict)
 97        {
 698            if (de.Key is null)
 99            {
 100                continue;
 101            }
 102
 6103            var k = de.Key.ToString();
 6104            if (string.IsNullOrWhiteSpace(k))
 105            {
 106                continue;
 107            }
 108
 6109            obj[k] = ToNode(de.Value);
 110        }
 111
 3112        return obj;
 113    }
 114
 115    /// <summary>
 116    /// Converts an IEnumerable to a JsonArray.
 117    /// </summary>
 118    /// <param name="en">The enumerable to convert.</param>
 119    /// <returns>A JsonArray representing the enumerable.</returns>
 120    private static JsonArray ToJsonArray(IEnumerable en)
 121    {
 8122        var arr = new JsonArray();
 16123        foreach (var item in en)
 124        {
 0125            arr.Add(ToNode(item));
 126        }
 8127        return arr;
 128    }
 129
 130    /// <summary>
 131    /// Converts a POCO or other object to a JsonNode by reflecting its public properties.
 132    /// </summary>
 133    /// <param name="value">The object to convert.</param>
 134    /// <returns>A JsonNode representing the object.</returns>
 135    private static JsonNode FromPocoOrString(object value)
 136    {
 12137        if (TryConvertPsObjectProperties(value, out var psObject))
 138        {
 0139            return psObject;
 140        }
 141
 12142        var type = value.GetType();
 143        // Skip null property values to avoid serializing them in the OpenAPI document.
 144        // Avoid reflecting on common framework types
 12145        if (ShouldFallbackToString(type))
 146        {
 0147            return JsonValue.Create(value.ToString() ?? string.Empty);
 148        }
 149
 12150        if (TryConvertPublicProperties(value, type, out var poco))
 151        {
 9152            return poco;
 153        }
 154        // Fallback to string representation
 3155        return JsonValue.Create(value.ToString() ?? string.Empty);
 156    }
 157
 158    /// <summary>
 159    /// Attempts to serialize a PowerShell object using its dynamic properties.
 160    /// </summary>
 161    /// <param name="value">The input value to inspect.</param>
 162    /// <param name="node">A JsonObject containing serialized PowerShell properties when successful.</param>
 163    /// <returns>True when properties were found and serialized; otherwise false.</returns>
 164    private static bool TryConvertPsObjectProperties(object value, out JsonNode node)
 165    {
 12166        node = null!;
 167
 168        // If it's a PowerShell object with properties, serialize those rather than reflection on the proxy type.
 12169        if (value is not PSObject pso)
 170        {
 12171            return false;
 172        }
 173
 0174        var obj = new JsonObject();
 0175        foreach (var prop in pso.Properties)
 176        {
 0177            var v = prop.Value;
 0178            if (v is null)
 179            {
 180                continue;
 181            }
 182
 0183            obj[prop.Name] = ToNode(v);
 184        }
 185
 0186        if (obj.Count == 0)
 187        {
 0188            return false;
 189        }
 190
 0191        node = obj;
 0192        return true;
 193    }
 194
 195    /// <summary>
 196    /// Determines whether an object of the specified type should be represented as a string
 197    /// instead of reflecting public properties.
 198    /// </summary>
 199    /// <param name="type">The type to inspect.</param>
 200    /// <returns>True when the type should be treated as a scalar/string; otherwise false.</returns>
 201    private static bool ShouldFallbackToString(Type type) =>
 12202        type.IsPrimitive || type == typeof(string) || typeof(IEnumerable).IsAssignableFrom(type);
 203
 204    /// <summary>
 205    /// Attempts to serialize a POCO by reflecting its public, readable, non-indexer instance properties.
 206    /// </summary>
 207    /// <param name="value">The object instance to read properties from.</param>
 208    /// <param name="type">The runtime type of <paramref name="value"/>.</param>
 209    /// <param name="node">A JsonObject when at least one property was serialized.</param>
 210    /// <returns>True when properties were found and serialized; otherwise false.</returns>
 211    private static bool TryConvertPublicProperties(object value, Type type, out JsonNode node)
 212    {
 12213        node = null!;
 214
 12215        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
 12216        if (props.Length == 0)
 217        {
 3218            return false;
 219        }
 220
 9221        var obj = new JsonObject();
 46222        foreach (var p in props)
 223        {
 14224            if (!IsReadableNonIndexer(p))
 225            {
 226                continue;
 227            }
 228
 14229            if (!TryGetPropertyValue(p, value, out var v) || v is null)
 230            {
 231                continue;
 232            }
 233
 14234            obj[p.Name] = ToNode(v);
 235        }
 236
 9237        if (obj.Count == 0)
 238        {
 0239            return false;
 240        }
 241
 9242        node = obj;
 9243        return true;
 244    }
 245
 246    /// <summary>
 247    /// Checks whether a property is readable and not an indexer.
 248    /// </summary>
 249    /// <param name="property">The property to check.</param>
 250    /// <returns>True when the property can be read and has no index parameters.</returns>
 251    private static bool IsReadableNonIndexer(PropertyInfo property) =>
 14252        property.CanRead && property.GetIndexParameters().Length == 0;
 253
 254    /// <summary>
 255    /// Attempts to read a property value and safely handles reflection exceptions.
 256    /// </summary>
 257    /// <param name="property">The property to read.</param>
 258    /// <param name="instance">The instance to read the value from.</param>
 259    /// <param name="value">The retrieved value, or null if not available.</param>
 260    /// <returns>True when the value was retrieved successfully; otherwise false.</returns>
 261    private static bool TryGetPropertyValue(PropertyInfo property, object instance, out object? value)
 262    {
 14263        value = null;
 264
 265        try
 266        {
 14267            value = property.GetValue(instance);
 14268            return true;
 269        }
 0270        catch
 271        {
 0272            return false;
 273        }
 14274    }
 275}