< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Runtime.PowerShellOpenApiClassExporter
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Runtime/PowerShellOpenApiClassExporter.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
91%
Covered lines: 96
Uncovered lines: 9
Coverable lines: 105
Total lines: 317
Line coverage: 91.4%
Branch coverage
75%
Covered branches: 44
Total branches: 58
Branch coverage: 75.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: 91.2% (94/103) Branch coverage: 75% (42/56) Total lines: 313 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 91.4% (96/105) Branch coverage: 75.8% (44/58) Total lines: 317 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff 12/12/2025 - 17:27:19 Line coverage: 91.2% (94/103) Branch coverage: 75% (42/56) Total lines: 313 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 91.4% (96/105) Branch coverage: 75.8% (44/58) Total lines: 317 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ValidClassNames()100%11100%
ExportOpenApiClasses()50%22100%
ExportOpenApiClasses(...)80%101094.44%
HasOpenApiComponentAttribute(...)50%22100%
AppendClass(...)100%66100%
ToPowerShellTypeName(...)59.09%382268.18%
TopologicalSortByPropertyDependencies(...)100%22100%
Visit(...)100%88100%
GetComponentDependencyType(...)83.33%6680%
WriteOpenApiTempScript(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Runtime/PowerShellOpenApiClassExporter.cs

#LineLine coverage
 1using System.Reflection;
 2using System.Text;
 3
 4namespace Kestrun.Runtime;
 5
 6/// <summary>
 7/// Exports OpenAPI component classes as PowerShell class definitions.
 8/// </summary>
 9public static class PowerShellOpenApiClassExporter
 10{
 11    /// <summary>
 12    /// Holds valid class names to be used as type in the OpenAPI function definitions.
 13    /// </summary>
 714    public static List<string> ValidClassNames { get; } = [];
 15
 16    /// <summary>
 17    /// Exports OpenAPI component classes found in loaded assemblies
 18    /// as PowerShell class definitions.
 19    /// </summary>
 20    /// <returns>The path to the temporary PowerShell script containing the class definitions.</returns>
 21    public static string ExportOpenApiClasses()
 22    {
 5923        var assemblies = AppDomain.CurrentDomain.GetAssemblies()
 1791624           .Where(a => a.FullName is not null &&
 1791625                    a.FullName.Contains("PowerShell Class Assembly"))
 5926           .ToArray();
 5927        return ExportOpenApiClasses(assemblies);
 28    }
 29
 30    /// <summary>
 31    /// Exports OpenAPI component classes found in the specified assemblies
 32    /// as PowerShell class definitions
 33    /// </summary>
 34    /// <param name="assemblies">The assemblies to scan for OpenAPI component classes.</param>
 35    /// <returns>The path to the temporary PowerShell script containing the class definitions.</returns>
 36    public static string ExportOpenApiClasses(Assembly[] assemblies)
 37    {
 38        // 1. Collect all component classes
 6039        var componentTypes = assemblies
 140            .SelectMany(a => a.GetTypes())
 341            .Where(t => t.IsClass && !t.IsAbstract)
 6042            .Where(HasOpenApiComponentAttribute)
 6043            .ToList();
 44
 45        // For quick lookup when choosing type names
 6046        var componentSet = new HashSet<Type>(componentTypes);
 47
 48        // 2. Topologically sort by "uses other component as property type"
 6049        var sorted = TopologicalSortByPropertyDependencies(componentTypes, componentSet);
 50        // nothing to export
 6051        if (sorted.Count == 0)
 52        {
 5953            return string.Empty;
 54        }
 55        // 3. Emit PowerShell classes
 156        var sb = new StringBuilder();
 57
 858        foreach (var type in sorted)
 59        {
 60            // Skip types without full name (should not happen)
 361            if (type.FullName is null)
 62            {
 63                continue;
 64            }
 365            if (ValidClassNames.Contains(type.FullName))
 66            {
 67                // Already registered remove old entry
 068                _ = ValidClassNames.Remove(type.FullName);
 69            }
 70            // Register valid class name
 371            ValidClassNames.Add(type.FullName);
 72            // Emit class definition
 373            AppendClass(type, componentSet, sb);
 374            _ = sb.AppendLine(); // blank line between classes
 75        }
 76        // 4. Write to temp script file
 177        return WriteOpenApiTempScript(sb.ToString());
 78    }
 79
 80    /// <summary>
 81    /// Determines if the specified type has an OpenAPI component attribute.
 82    /// </summary>
 83    /// <param name="t"></param>
 84    /// <returns></returns>
 85    private static bool HasOpenApiComponentAttribute(Type t)
 86    {
 387        return t.GetCustomAttributes(inherit: true)
 388                .Select(a => a.GetType().Name)
 389                .Any(n =>
 690                    n.Contains("OpenApiSchemaComponent", StringComparison.OrdinalIgnoreCase) ||
 691                    n.Contains("OpenApiRequestBodyComponent", StringComparison.OrdinalIgnoreCase));
 92    }
 93
 94    /// <summary>
 95    /// Appends the PowerShell class definition for the specified type to the StringBuilder.
 96    /// </summary>
 97    /// <param name="type"></param>
 98    /// <param name="componentSet"></param>
 99    /// <param name="sb"></param>
 100    private static void AppendClass(Type type, HashSet<Type> componentSet, StringBuilder sb)
 101    {
 102        // Detect base type (for parenting)
 3103        var baseType = type.BaseType;
 3104        var baseClause = string.Empty;
 105
 3106        if (baseType != null && baseType != typeof(object))
 107        {
 108            // Use PS-friendly type name for the base
 1109            var basePsName = ToPowerShellTypeName(baseType, componentSet);
 1110            baseClause = $" : {basePsName}";
 111        }
 112
 3113        _ = sb.AppendLine($"class {type.Name}{baseClause} {{");
 114
 115        // Only properties *declared* on this type (no inherited ones)
 3116        var props = type.GetProperties(
 3117            BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
 118
 14119        foreach (var p in props)
 120        {
 4121            var psType = ToPowerShellTypeName(p.PropertyType, componentSet);
 4122            _ = sb.AppendLine($"    [{psType}]${p.Name}");
 123        }
 124
 3125        _ = sb.AppendLine("}");
 3126    }
 127
 128    /// <summary>
 129    /// Converts a .NET type to a PowerShell type name.
 130    /// </summary>
 131    /// <param name="t"></param>
 132    /// <param name="componentSet"></param>
 133    /// <returns></returns>
 134    private static string ToPowerShellTypeName(Type t, HashSet<Type> componentSet)
 135    {
 136        // Nullable<T>
 6137        if (Nullable.GetUnderlyingType(t) is Type underlying)
 138        {
 0139            return $"Nullable[{ToPowerShellTypeName(underlying, componentSet)}]";
 140        }
 141
 142        // Primitive mappings
 6143        if (t == typeof(long))
 144        {
 0145            return "long";
 146        }
 147
 6148        if (t == typeof(int))
 149        {
 1150            return "int";
 151        }
 152
 5153        if (t == typeof(bool))
 154        {
 0155            return "bool";
 156        }
 157
 5158        if (t == typeof(string))
 159        {
 2160            return "string";
 161        }
 162
 3163        if (t == typeof(double))
 164        {
 0165            return "double";
 166        }
 167
 3168        if (t == typeof(float))
 169        {
 0170            return "single";
 171        }
 172
 3173        if (t == typeof(object))
 174        {
 0175            return "object";
 176        }
 177
 178        // Arrays
 3179        if (t.IsArray)
 180        {
 1181            var element = ToPowerShellTypeName(t.GetElementType()!, componentSet);
 1182            return $"{element}[]";
 183        }
 184
 185        // If the property type is itself one of the OpenAPI component classes,
 186        // use its *simple* name (Pet, User, Tag, Category, etc.)
 2187        if (componentSet.Contains(t))
 188        {
 2189            return t.Name;
 190        }
 191
 192        // Fallback for other reference types (you can change to t.Name if you prefer)
 0193        return t.FullName ?? t.Name;
 194    }
 195
 196    /// <summary>
 197    /// Topologically sort types so that dependencies (property types)
 198    /// appear before the types that reference them.
 199    /// </summary>
 200    /// <param name="types">The list of types to sort.</param>
 201    /// <param name="componentSet">Set of component types for quick lookup.</param>
 202    /// <returns>The sorted list of types.</returns>
 203    private static List<Type> TopologicalSortByPropertyDependencies(
 204        List<Type> types,
 205        HashSet<Type> componentSet)
 206    {
 60207        var result = new List<Type>();
 60208        var visited = new Dictionary<Type, bool>(); // false = temp-mark, true = perm-mark
 209
 126210        foreach (var t in types)
 211        {
 3212            Visit(t, componentSet, visited, result);
 213        }
 214
 60215        return result;
 216    }
 217
 218    /// <summary>
 219    /// Visits the type and its dependencies recursively for topological sorting.
 220    /// </summary>
 221    /// <param name="t">Type to visit</param>
 222    /// <param name="componentSet">Set of component types</param>
 223    /// <param name="visited">Dictionary tracking visited types and their mark status</param>
 224    /// <param name="result">List to accumulate the sorted types</param>
 225    private static void Visit(
 226     Type t,
 227     HashSet<Type> componentSet,
 228     Dictionary<Type, bool> visited,
 229     List<Type> result)
 230    {
 5231        if (visited.TryGetValue(t, out var perm))
 232        {
 2233            if (!perm)
 234            {
 235                // cycle; ignore for now
 2236                return;
 237            }
 238            return;
 239        }
 240
 241        // temp-mark
 3242        visited[t] = false;
 243
 3244        var deps = new List<Type>();
 245
 246        // 1) Dependencies via property types (component properties)
 3247        var propDeps = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 5248                        .Select(p => GetComponentDependencyType(p.PropertyType, componentSet))
 5249                        .Where(dep => dep is not null)
 1250                        .Select(dep => dep!)
 3251                        .Distinct();
 252
 3253        deps.AddRange(propDeps);
 254
 255        // 2) Dependency via base type (parenting)
 3256        var baseType = t.BaseType;
 3257        if (baseType != null && componentSet.Contains(baseType))
 258        {
 1259            deps.Add(baseType);
 260        }
 261
 10262        foreach (var dep in deps.Distinct())
 263        {
 2264            Visit(dep, componentSet, visited, result);
 265        }
 266
 267        // perm-mark
 3268        visited[t] = true;
 3269        result.Add(t);
 3270    }
 271
 272    private static Type? GetComponentDependencyType(Type propertyType, HashSet<Type> componentSet)
 273    {
 274        // Unwrap Nullable
 5275        if (Nullable.GetUnderlyingType(propertyType) is Type underlying)
 276        {
 0277            propertyType = underlying;
 278        }
 279
 280        // Unwrap arrays
 5281        if (propertyType.IsArray)
 282        {
 1283            propertyType = propertyType.GetElementType()!;
 284        }
 285
 5286        return componentSet.Contains(propertyType) ? propertyType : null;
 287    }
 288
 289    /// <summary>
 290    /// Writes the OpenAPI class definitions to a temporary PowerShell script file.
 291    /// </summary>
 292    /// <param name="openApiClasses">The OpenAPI class definitions as a string.</param>
 293    /// <returns>The path to the temporary PowerShell script file.</returns>
 294    public static string WriteOpenApiTempScript(string openApiClasses)
 295    {
 296        // Use a stable file name so multiple runspaces share the same script
 1297        var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ps1");
 298
 299        // Ensure directory exists
 1300        _ = Directory.CreateDirectory(Path.GetDirectoryName(tempPath)!);
 301
 302        // Build content with header
 1303        var sb = new StringBuilder()
 1304        .AppendLine("# ================================================")
 1305        .AppendLine("#   Kestrun OpenAPI Autogenerated Class Definitions")
 1306        .AppendLine("#   DO NOT EDIT - generated at runtime")
 1307        .Append("#   Timestamp: ").Append(DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append('Z').AppendLine()
 1308        .AppendLine("# ================================================")
 1309        .AppendLine()
 1310        .AppendLine(openApiClasses);
 311
 312        // Save using UTF-8 without BOM
 1313        File.WriteAllText(tempPath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
 314
 1315        return tempPath;
 316    }
 317}