< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Forms.FormHelper
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Forms/FormHelper.cs
Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d9589
Line coverage
89%
Covered lines: 139
Uncovered lines: 16
Coverable lines: 155
Total lines: 408
Line coverage: 89.6%
Branch coverage
83%
Covered branches: 98
Total branches: 118
Branch coverage: 83%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 02/05/2026 - 00:28:18 Line coverage: 89.6% (139/155) Branch coverage: 83% (98/118) Total lines: 408 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d9589 02/05/2026 - 00:28:18 Line coverage: 89.6% (139/155) Branch coverage: 83% (98/118) Total lines: 408 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d9589

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
PopulateNestedRulesFromScopes(...)95.83%242490.9%
ApplyKrPartAttributes(...)70%101095.45%
ResolvePartName(...)71.42%271460%
BuildFormPartRulesFromType(...)100%11100%
BuildFormPartRulesFromType(...)87.5%8890.9%
TryBuildRuleInfo(...)83.33%121291.66%
AddNestedRules(...)100%22100%
get_Rule()100%11100%
UnwrapElementType(...)66.66%6683.33%
IsMultipartContainerRule(...)80%101085.71%
IsComplexType(...)50%44100%
AttachNestedRule(...)70%111081.81%
ApplyKrPartAttributes(...)100%1818100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Forms/FormHelper.cs

#LineLine coverage
 1using System.Reflection;
 2
 3namespace Kestrun.Forms;
 4
 5/// <summary>
 6/// Provides helper methods for form handling.
 7/// </summary>
 8internal static class FormHelper
 9{
 10    /// <summary>
 11    /// Populates <see cref="KrFormPartRule.NestedRules"/> for rules within a <see cref="KrFormOptions"/> instance.
 12    /// This uses <see cref="KrFormPartRule.Scope"/> as the parent container rule name and attaches all scoped rules
 13    /// to their matching container rule(s) (case-insensitive).
 14    /// </summary>
 15    /// <param name="options">The options containing rules to link.</param>
 16    internal static void PopulateNestedRulesFromScopes(KrFormOptions options)
 17    {
 518        ArgumentNullException.ThrowIfNull(options);
 19
 20        // Clear first to avoid duplication when called multiple times.
 5021        foreach (var rule in options.Rules)
 22        {
 2023            rule.NestedRules.Clear();
 24        }
 25
 26        // Build a lookup of container-name -> list of container rules.
 527        var containers = new Dictionary<string, List<KrFormPartRule>>(StringComparer.OrdinalIgnoreCase);
 5028        foreach (var rule in options.Rules)
 29        {
 2030            if (string.IsNullOrWhiteSpace(rule.Name))
 31            {
 32                continue;
 33            }
 34
 2035            if (!containers.TryGetValue(rule.Name, out var bag))
 36            {
 2037                bag = [];
 2038                containers.Add(rule.Name, bag);
 39            }
 40
 2041            bag.Add(rule);
 42        }
 43
 5044        foreach (var child in options.Rules)
 45        {
 2046            if (string.IsNullOrWhiteSpace(child.Scope))
 47            {
 48                continue;
 49            }
 50
 1051            if (!containers.TryGetValue(child.Scope, out var parents) || parents.Count == 0)
 52            {
 53                continue;
 54            }
 55
 4056            foreach (var parent in parents)
 57            {
 1058                var exists = false;
 3059                for (var i = 0; i < parent.NestedRules.Count; i++)
 60                {
 561                    if (ReferenceEquals(parent.NestedRules[i], child))
 62                    {
 063                        exists = true;
 064                        break;
 65                    }
 66                }
 67
 1068                if (!exists)
 69                {
 1070                    parent.NestedRules.Add(child);
 71                }
 72            }
 73        }
 574    }
 75
 76    /// <summary>
 77    /// Applies KrPartAttribute annotations to the host's form part rules.
 78    /// </summary>
 79    /// <param name="host">The Kestrun host runtime.</param>
 80    /// <param name="p">The property info to inspect for KrPartAttribute annotations.</param>
 81    /// <param name="scopeName">The parent multipart scope name (null for root).</param>
 82    internal static void ApplyKrPartAttributes(Hosting.KestrunHost host, PropertyInfo p, string? scopeName)
 83    {
 16884        var name = ResolvePartName(p);
 85        // Apply KrPartAttribute annotations
 37286        foreach (var attr in p.GetCustomAttributes<KrPartAttribute>(inherit: false))
 87        {
 1888            var formPartRule = new KrFormPartRule
 1889            {
 1890                Name = name ?? "",
 1891                Scope = scopeName,
 1892                Description = attr.Description,
 1893                Required = attr.Required,
 1894                AllowMultiple = attr.AllowMultiple,
 1895
 1896                MaxBytes = attr.MaxBytes > 0 ? attr.MaxBytes : null,
 1897                DecodeMode = attr.DecodeMode,
 1898                DestinationPath = attr.DestinationPath,
 1899                StoreToDisk = attr.StoreToDisk,
 18100            };
 101
 18102            formPartRule.AllowedContentTypes.AddRange(attr.ContentTypes);
 18103            formPartRule.AllowedExtensions.AddRange(attr.Extensions);
 18104            var added = host.AddFormPartRule(formPartRule);
 18105            if (!added && host.Runtime.FormPartRules.TryGetValue(formPartRule.Name, out var existingRule))
 106            {
 0107                formPartRule = existingRule;
 108            }
 18109            AttachNestedRule(host, scopeName, formPartRule);
 110        }
 168111    }
 112
 113    /// <summary>
 114    /// Resolves the part name for a property, including PowerShell dynamic type handling.
 115    /// </summary>
 116    /// <param name="p">The property info to inspect.</param>
 117    /// <returns>The resolved part name.</returns>
 118    internal static string? ResolvePartName(PropertyInfo p)
 119    {
 205120        var name = p.Name;
 205121        var type = p.PropertyType;
 122
 123        // Arrays / collections → unwrap
 205124        if (type.IsArray)
 125        {
 21126            type = type.GetElementType()!;
 127        }
 128        // Nullable<T> → unwrap
 205129        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
 130        {
 8131            type = Nullable.GetUnderlyingType(type)!;
 132        }
 133
 205134        var asm = type.Assembly;
 135
 136        // PowerShell classes are emitted into a dynamic assembly
 205137        if (asm.IsDynamic)
 138        {
 139            // Stable PowerShell assembly naming convention
 0140            var fullName = asm.FullName;
 0141            if (fullName != null &&
 0142                  fullName.StartsWith("PowerShell Class Assembly", StringComparison.Ordinal))
 143            {
 144                // PropertyType.FullName can be null for some dynamic types; avoid dereference
 0145                var propFullName = p.PropertyType.FullName;
 0146                if (propFullName is not null)
 147                {
 0148                    name = propFullName.Trim('[', ']');
 149                }
 150            }
 151        }
 152
 205153        return name;
 154    }
 155
 156    /// <summary>
 157    /// Builds form part rules for a CLR type decorated with <see cref="KrPartAttribute"/>.
 158    /// Nested multipart container parts (parts whose allowed content types include <c>multipart/*</c>)
 159    /// will have their inner part rules added to <see cref="KrFormPartRule.NestedRules"/>, and the
 160    /// inner rules will be scoped to the container's part name.
 161    /// </summary>
 162    /// <param name="rootType">The root type to inspect.</param>
 163    /// <returns>A flattened list of rules including nested rules.</returns>
 164    internal static List<KrFormPartRule> BuildFormPartRulesFromType(Type rootType)
 165    {
 6166        ArgumentNullException.ThrowIfNull(rootType);
 6167        var visited = new HashSet<Type>();
 6168        return BuildFormPartRulesFromType(rootType, scopeName: null, visited);
 169    }
 170
 171    /// <summary>
 172    /// Builds form part rules for a type, scoped under a given parent container name.
 173    /// </summary>
 174    /// <param name="type">The type to inspect.</param>
 175    /// <param name="scopeName">The parent multipart scope name.</param>
 176    /// <param name="visited">The set of visited types to avoid cycles.</param>
 177    /// <returns>A flattened list of rules including nested rules.</returns>
 178    private static List<KrFormPartRule> BuildFormPartRulesFromType(Type type, string? scopeName, HashSet<Type> visited)
 179    {
 11180        if (!visited.Add(type))
 181        {
 0182            return [];
 183        }
 184
 11185        var rules = new List<KrFormPartRule>();
 60186        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 30187                     .Where(static p => p.DeclaringType == p.ReflectedType))
 188        {
 19189            var ruleInfo = TryBuildRuleInfo(prop, scopeName);
 19190            if (ruleInfo is not { } info)
 191            {
 192                continue;
 193            }
 194
 19195            rules.Add(info.Rule);
 19196            if (info.CanNest)
 197            {
 5198                AddNestedRules(info.Rule, info.UnderlyingType!, visited, rules);
 199            }
 200        }
 201
 11202        return rules;
 203    }
 204
 205    /// <summary>
 206    /// Creates rule metadata for a property decorated with <see cref="KrPartAttribute"/>.
 207    /// </summary>
 208    /// <param name="prop">The property to inspect.</param>
 209    /// <param name="scopeName">The parent multipart scope name.</param>
 210    /// <returns>The rule info or null when no rule should be created.</returns>
 211    private static RuleInfo? TryBuildRuleInfo(PropertyInfo prop, string? scopeName)
 212    {
 19213        var attr = prop.GetCustomAttribute<KrPartAttribute>(inherit: false);
 19214        if (attr is null)
 215        {
 0216            return null;
 217        }
 218
 19219        var partName = ResolvePartName(prop);
 19220        if (string.IsNullOrWhiteSpace(partName))
 221        {
 0222            return null;
 223        }
 224
 19225        var underlying = UnwrapElementType(prop.PropertyType);
 19226        var canNest = IsMultipartContainerRule(attr) && underlying is not null && IsComplexType(underlying);
 19227        var ruleName = canNest ? underlying!.Name : partName;
 228
 19229        var rule = new KrFormPartRule
 19230        {
 19231            Name = ruleName,
 19232            Scope = scopeName,
 19233            Description = attr.Description,
 19234            Required = attr.Required,
 19235            AllowMultiple = attr.AllowMultiple,
 19236            MaxBytes = attr.MaxBytes > 0 ? attr.MaxBytes : null,
 19237            DecodeMode = attr.DecodeMode,
 19238            DestinationPath = attr.DestinationPath,
 19239            StoreToDisk = attr.StoreToDisk,
 19240        };
 241
 19242        rule.AllowedContentTypes.AddRange(attr.ContentTypes);
 19243        rule.AllowedExtensions.AddRange(attr.Extensions);
 244
 19245        return new RuleInfo(rule, underlying, canNest);
 246    }
 247
 248    /// <summary>
 249    /// Adds nested rules for a multipart container rule and flattens them into the output list.
 250    /// </summary>
 251    /// <param name="parent">The parent container rule.</param>
 252    /// <param name="underlying">The underlying complex type to inspect.</param>
 253    /// <param name="visited">The set of visited types to avoid cycles.</param>
 254    /// <param name="rules">The master rules list to append to.</param>
 255    private static void AddNestedRules(
 256        KrFormPartRule parent,
 257        Type underlying,
 258        HashSet<Type> visited,
 259        List<KrFormPartRule> rules)
 260    {
 5261        var childRules = BuildFormPartRulesFromType(underlying, scopeName: parent.Name, visited);
 28262        foreach (var child in childRules)
 263        {
 9264            parent.NestedRules.Add(child);
 265        }
 266
 5267        rules.AddRange(childRules);
 5268    }
 269
 270    /// <summary>
 271    /// Holds rule construction details for a property.
 272    /// </summary>
 48273    private readonly record struct RuleInfo(KrFormPartRule Rule, Type? UnderlyingType, bool CanNest);
 274
 275    /// <summary>
 276    /// Unwraps array or nullable types to their element or underlying type.
 277    /// </summary>
 278    /// <param name="type">The type to unwrap.</param>
 279    /// <returns>The unwrapped element or underlying type, or the original type if not an array or nullable.</returns>
 280    private static Type? UnwrapElementType(Type type)
 281    {
 19282        var t = type;
 19283        if (t.IsArray)
 284        {
 4285            t = t.GetElementType()!;
 286        }
 287
 19288        if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
 289        {
 0290            t = Nullable.GetUnderlyingType(t)!;
 291        }
 292
 19293        return t;
 294    }
 295
 296    private static bool IsMultipartContainerRule(KrPartAttribute attr)
 297    {
 19298        if (attr.ContentTypes is null || attr.ContentTypes.Length == 0)
 299        {
 0300            return false;
 301        }
 302
 71303        foreach (var ct in attr.ContentTypes)
 304        {
 19305            if (ct is null)
 306            {
 307                continue;
 308            }
 309
 19310            if (ct.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase))
 311            {
 5312                return true;
 313            }
 314        }
 315
 14316        return false;
 317    }
 318
 5319    private static bool IsComplexType(Type type) => !type.IsEnum && type != typeof(string) && !type.IsPrimitive;
 320
 321    /// <summary>
 322    /// Attaches a rule to its parent container rule when a scope is provided.
 323    /// </summary>
 324    /// <param name="host">The Kestrun host runtime.</param>
 325    /// <param name="scopeName">The parent multipart scope name.</param>
 326    /// <param name="childRule">The rule to attach.</param>
 327    private static void AttachNestedRule(Hosting.KestrunHost host, string? scopeName, KrFormPartRule childRule)
 328    {
 18329        if (string.IsNullOrWhiteSpace(scopeName))
 330        {
 15331            return;
 332        }
 333
 3334        if (!host.Runtime.FormPartRules.TryGetValue(scopeName, out var parentRule))
 335        {
 0336            return;
 337        }
 338
 8339        for (var i = 0; i < parentRule.NestedRules.Count; i++)
 340        {
 1341            var existing = parentRule.NestedRules[i];
 1342            if (string.Equals(existing.Name, childRule.Name, StringComparison.OrdinalIgnoreCase) &&
 1343                string.Equals(existing.Scope, childRule.Scope, StringComparison.OrdinalIgnoreCase))
 344            {
 0345                return;
 346            }
 347        }
 348
 3349        parentRule.NestedRules.Add(childRule);
 3350    }
 351
 352    /// <summary>
 353    /// Applies KrBindFormAttribute to form options.
 354    /// </summary>
 355    /// <param name="attr">The KrBindFormAttribute instance to apply.</param>
 356    /// <returns>A KrFormOptions object configured based on the attribute.</returns>
 357    internal static KrFormOptions ApplyKrPartAttributes(KrBindFormAttribute attr)
 358    {
 5359        var formOptions = new KrFormOptions
 5360        {
 5361            ComputeSha256 = attr.ComputeSha256,
 5362            EnablePartDecompression = attr.EnablePartDecompression,
 5363            RejectUnknownRequestContentType = attr.RejectUnknownRequestContentType,
 5364            RejectUnknownContentEncoding = attr.RejectUnknownContentEncoding
 5365        };
 366
 5367        if (attr.DefaultUploadPath is not null)
 368        {
 1369            formOptions.DefaultUploadPath = attr.DefaultUploadPath;
 370        }
 371
 5372        if (attr.MaxDecompressedBytesPerPart > 0)
 373        {
 1374            formOptions.MaxDecompressedBytesPerPart = attr.MaxDecompressedBytesPerPart;
 375        }
 376
 5377        if (attr.AllowedPartContentEncodings is not null)
 378        {
 1379            formOptions.AllowedPartContentEncodings.Clear();
 1380            formOptions.AllowedPartContentEncodings.AddRange(attr.AllowedPartContentEncodings);
 381        }
 5382        if (attr.MaxRequestBodyBytes > 0)
 383        {
 1384            formOptions.Limits.MaxRequestBodyBytes = attr.MaxRequestBodyBytes;
 385        }
 5386        if (attr.MaxPartBodyBytes > 0)
 387        {
 1388            formOptions.Limits.MaxPartBodyBytes = attr.MaxPartBodyBytes;
 389        }
 5390        if (attr.MaxParts > 0)
 391        {
 1392            formOptions.Limits.MaxParts = attr.MaxParts;
 393        }
 5394        if (attr.MaxHeaderBytesPerPart > 0)
 395        {
 1396            formOptions.Limits.MaxHeaderBytesPerPart = attr.MaxHeaderBytesPerPart;
 397        }
 5398        if (attr.MaxFieldValueBytes > 0)
 399        {
 1400            formOptions.Limits.MaxFieldValueBytes = attr.MaxFieldValueBytes;
 401        }
 5402        if (attr.MaxNestingDepth > 0)
 403        {
 5404            formOptions.Limits.MaxNestingDepth = attr.MaxNestingDepth;
 405        }
 5406        return formOptions;
 407    }
 408}