< 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
 219    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 220        new ReadOnlyDictionary<string, string>(
 221            new Dictionary<string, string>(StringComparer.Ordinal));
 22
 223    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    {
 22631        if (metadata is null || metadata.Count == 0)
 32        {
 1733            return EmptyMetadata;
 34        }
 35
 20936        Dictionary<string, string> normalizedMetadata = new(metadata.Count, StringComparer.Ordinal);
 37
 85238        foreach (KeyValuePair<string, string> item in metadata)
 39        {
 21740            if (string.IsNullOrWhiteSpace(item.Key))
 41            {
 42                continue;
 43            }
 44
 20645            normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 46        }
 47
 20948        return normalizedMetadata.Count == 0
 20949            ? EmptyMetadata
 20950            : 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    {
 1560        GovernanceMetadataBudget activeBudget = budget ?? GovernanceMetadataBudget.Recommended;
 1561        IReadOnlyDictionary<string, string> normalizedMetadata = Normalize(metadata);
 1562        List<string>? violations = null;
 63
 1564        if (normalizedMetadata.Count > activeBudget.MaxCount)
 65        {
 266            AddViolation(
 267                ref violations,
 268                $"Metadata count {normalizedMetadata.Count} exceeds maximum metadata count {activeBudget.MaxCount}.");
 69        }
 70
 5471        foreach (KeyValuePair<string, string> item in normalizedMetadata)
 72        {
 1273            if (item.Key.Length > activeBudget.MaxKeyLength)
 74            {
 175                AddViolation(
 176                    ref violations,
 177                    $"Metadata key '{item.Key}' length {item.Key.Length} exceeds maximum key length {activeBudget.MaxKey
 78            }
 79
 1280            if (item.Value.Length > activeBudget.MaxValueLength)
 81            {
 382                AddViolation(
 383                    ref violations,
 384                    $"Metadata value for key '{item.Key}' length {item.Value.Length} exceeds maximum value length {activ
 85            }
 86
 1287            string? reservedFragment = FindReservedKeyFragment(item.Key, activeBudget);
 1288            if (reservedFragment is not null)
 89            {
 290                AddViolation(
 291                    ref violations,
 292                    $"Metadata key '{item.Key}' matches reserved or discouraged key fragment '{reservedFragment}'. Store
 93            }
 94        }
 95
 1596        int estimatedSerializedBytes = EstimateSerializedSizeBytesCore(normalizedMetadata);
 1597        if (estimatedSerializedBytes > activeBudget.MaxSerializedBytes)
 98        {
 199            AddViolation(
 1100                ref violations,
 1101                $"Estimated serialized metadata size {estimatedSerializedBytes} bytes exceeds maximum serialized metadat
 102        }
 103
 15104        return GovernanceMetadataBudgetValidationResult.Create(
 15105            normalizedMetadata,
 15106            violations is null ? EmptyViolations : Array.AsReadOnly([.. violations]),
 15107            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    {
 2118        GovernanceMetadataBudgetValidationResult result = Validate(metadata, budget);
 2119        result.ThrowIfInvalid(parameterName ?? nameof(metadata));
 1120        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    {
 1128        return EstimateSerializedSizeBytesCore(Normalize(metadata));
 129    }
 130
 131    private static int EstimateSerializedSizeBytesCore(IReadOnlyDictionary<string, string> normalizedMetadata)
 132    {
 16133        if (normalizedMetadata.Count == 0)
 134        {
 5135            return EmptySerializedObjectBytes;
 136        }
 137
 11138        int totalBytes = EmptySerializedObjectBytes;
 139
 48140        foreach (KeyValuePair<string, string> item in normalizedMetadata)
 141        {
 13142            totalBytes += SerializedEntryOverheadBytes;
 13143            totalBytes += Encoding.UTF8.GetByteCount(item.Key);
 13144            totalBytes += Encoding.UTF8.GetByteCount(item.Value);
 145        }
 146
 11147        return totalBytes;
 148    }
 149
 150    private static string? FindReservedKeyFragment(string key, GovernanceMetadataBudget budget)
 151    {
 12152        if (budget.ReservedKeyFragments.Count == 0)
 153        {
 7154            return null;
 155        }
 156
 5157        string normalizedKey = GovernanceMetadataBudget.NormalizeKeyForComparison(key);
 158
 88159        foreach (string reservedFragment in budget.ReservedKeyFragments)
 160        {
 40161            if (normalizedKey.Contains(reservedFragment, StringComparison.Ordinal))
 162            {
 2163                return reservedFragment;
 164            }
 165        }
 166
 3167        return null;
 2168    }
 169
 170    private static void AddViolation(ref List<string>? violations, string violation)
 171    {
 9172        violations ??= [];
 9173        violations.Add(violation);
 9174    }
 175}