< Summary

Information
Class: AsiBackbone.Core.Metadata.GovernanceMetadataBudgetValidator
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Metadata/GovernanceMetadataBudgetValidator.cs
Line coverage
100%
Covered lines: 66
Uncovered lines: 0
Coverable lines: 66
Total lines: 175
Line coverage: 100%
Branch coverage
93%
Covered branches: 41
Total branches: 44
Branch coverage: 93.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Normalize(...)85.71%1414100%
Validate(...)100%1616100%
NormalizeAndValidate(...)50%22100%
EstimateSerializedSizeBytes(...)100%11100%
EstimateSerializedSizeBytesCore(...)100%44100%
FindReservedKeyFragment(...)100%66100%
AddViolation(...)100%22100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Metadata/GovernanceMetadataBudgetValidator.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using System.Text;
 3
 4namespace AsiBackbone.Core.Metadata;
 5
 6/// <summary>
 7/// Provides optional normalization and budget validation helpers for host-owned governance metadata.
 8/// </summary>
 9/// <remarks>
 10/// This helper does not classify, redact, encrypt, or sanitize sensitive values. Hosts should treat
 11/// successful validation as a bounded-shape check only and still apply their own privacy, DLP,
 12/// retention, and signing policies before durable storage or emission.
 13/// </remarks>
 14public static class GovernanceMetadataBudgetValidator
 15{
 16    private const int EmptySerializedObjectBytes = 2;
 17    private const int SerializedEntryOverheadBytes = 6;
 18
 319    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 320        new ReadOnlyDictionary<string, string>(
 321            new Dictionary<string, string>(StringComparer.Ordinal));
 22
 323    private static readonly string[] EmptyViolations = [];
 24
 25    /// <summary>
 26    /// Normalizes metadata by trimming keys and values, removing blank keys, and using ordinal key comparison.
 27    /// </summary>
 28    public static IReadOnlyDictionary<string, string> Normalize(
 29        IReadOnlyDictionary<string, string>? metadata)
 30    {
 44931        if (metadata is null || metadata.Count == 0)
 32        {
 2833            return EmptyMetadata;
 34        }
 35
 42136        Dictionary<string, string> normalizedMetadata = new(metadata.Count, StringComparer.Ordinal);
 37
 170638        foreach (KeyValuePair<string, string> item in metadata)
 39        {
 43240            if (string.IsNullOrWhiteSpace(item.Key))
 41            {
 42                continue;
 43            }
 44
 41445            normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 46        }
 47
 42148        return normalizedMetadata.Count == 0
 42149            ? EmptyMetadata
 42150            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 51    }
 52
 53    /// <summary>
 54    /// Validates metadata against the recommended budget or a host-supplied budget.
 55    /// </summary>
 56    public static GovernanceMetadataBudgetValidationResult Validate(
 57        IReadOnlyDictionary<string, string>? metadata,
 58        GovernanceMetadataBudget? budget = null)
 59    {
 2960        GovernanceMetadataBudget activeBudget = budget ?? GovernanceMetadataBudget.Recommended;
 2961        IReadOnlyDictionary<string, string> normalizedMetadata = Normalize(metadata);
 2962        List<string>? violations = null;
 63
 2964        if (normalizedMetadata.Count > activeBudget.MaxCount)
 65        {
 466            AddViolation(
 467                ref violations,
 468                $"Metadata count {normalizedMetadata.Count} exceeds maximum metadata count {activeBudget.MaxCount}.");
 69        }
 70
 10671        foreach (KeyValuePair<string, string> item in normalizedMetadata)
 72        {
 2473            if (item.Key.Length > activeBudget.MaxKeyLength)
 74            {
 275                AddViolation(
 276                    ref violations,
 277                    $"Metadata key '{item.Key}' length {item.Key.Length} exceeds maximum key length {activeBudget.MaxKey
 78            }
 79
 2480            if (item.Value.Length > activeBudget.MaxValueLength)
 81            {
 682                AddViolation(
 683                    ref violations,
 684                    $"Metadata value for key '{item.Key}' length {item.Value.Length} exceeds maximum value length {activ
 85            }
 86
 2487            string? reservedFragment = FindReservedKeyFragment(item.Key, activeBudget);
 2488            if (reservedFragment is not null)
 89            {
 490                AddViolation(
 491                    ref violations,
 492                    $"Metadata key '{item.Key}' matches reserved or discouraged key fragment '{reservedFragment}'. Store
 93            }
 94        }
 95
 2996        int estimatedSerializedBytes = EstimateSerializedSizeBytesCore(normalizedMetadata);
 2997        if (estimatedSerializedBytes > activeBudget.MaxSerializedBytes)
 98        {
 299            AddViolation(
 2100                ref violations,
 2101                $"Estimated serialized metadata size {estimatedSerializedBytes} bytes exceeds maximum serialized metadat
 102        }
 103
 29104        return GovernanceMetadataBudgetValidationResult.Create(
 29105            normalizedMetadata,
 29106            violations is null ? EmptyViolations : Array.AsReadOnly([.. violations]),
 29107            estimatedSerializedBytes);
 108    }
 109
 110    /// <summary>
 111    /// Normalizes and validates metadata, throwing when the supplied metadata exceeds the budget.
 112    /// </summary>
 113    public static IReadOnlyDictionary<string, string> NormalizeAndValidate(
 114        IReadOnlyDictionary<string, string>? metadata,
 115        GovernanceMetadataBudget? budget = null,
 116        string? parameterName = null)
 117    {
 4118        GovernanceMetadataBudgetValidationResult result = Validate(metadata, budget);
 4119        result.ThrowIfInvalid(parameterName ?? nameof(metadata));
 2120        return result.NormalizedMetadata;
 121    }
 122
 123    /// <summary>
 124    /// Estimates the UTF-8 serialized size of normalized metadata for budget comparison.
 125    /// </summary>
 126    public static int EstimateSerializedSizeBytes(IReadOnlyDictionary<string, string>? metadata)
 127    {
 2128        return EstimateSerializedSizeBytesCore(Normalize(metadata));
 129    }
 130
 131    private static int EstimateSerializedSizeBytesCore(IReadOnlyDictionary<string, string> normalizedMetadata)
 132    {
 31133        if (normalizedMetadata.Count == 0)
 134        {
 9135            return EmptySerializedObjectBytes;
 136        }
 137
 22138        int totalBytes = EmptySerializedObjectBytes;
 139
 96140        foreach (KeyValuePair<string, string> item in normalizedMetadata)
 141        {
 26142            totalBytes += SerializedEntryOverheadBytes;
 26143            totalBytes += Encoding.UTF8.GetByteCount(item.Key);
 26144            totalBytes += Encoding.UTF8.GetByteCount(item.Value);
 145        }
 146
 22147        return totalBytes;
 148    }
 149
 150    private static string? FindReservedKeyFragment(string key, GovernanceMetadataBudget budget)
 151    {
 24152        if (budget.ReservedKeyFragments.Count == 0)
 153        {
 14154            return null;
 155        }
 156
 10157        string normalizedKey = GovernanceMetadataBudget.NormalizeKeyForComparison(key);
 158
 176159        foreach (string reservedFragment in budget.ReservedKeyFragments)
 160        {
 80161            if (normalizedKey.Contains(reservedFragment, StringComparison.Ordinal))
 162            {
 4163                return reservedFragment;
 164            }
 165        }
 166
 6167        return null;
 4168    }
 169
 170    private static void AddViolation(ref List<string>? violations, string violation)
 171    {
 18172        violations ??= [];
 18173        violations.Add(violation);
 18174    }
 175}