< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Channels.NATS.NatsSubjectSchema
Assembly: AsyncResponse.Channels.NATS
File(s): /_/src/Channels/AsyncResponse.Channels.NATS/NatsSubjectSchema.cs
Line coverage
100%
Covered lines: 19
Uncovered lines: 0
Coverable lines: 19
Total lines: 77
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ResponseSubject(...)100%11100%
RecoveryKey(...)100%11100%
CorrelationIdFromRecoveryKey(...)100%22100%
.cctor()100%11100%
Encode(...)100%11100%
Decode(...)100%66100%

File(s)

/_/src/Channels/AsyncResponse.Channels.NATS/NatsSubjectSchema.cs

#LineLine coverage
 1using System.Text;
 2
 3namespace AsyncResponse.Channels.NATS;
 4
 5/// <summary>
 6/// The single source of truth for NATS subject and Key-Value key shapes used by the channel and the
 7/// recovery store. Correlation ids are arbitrary strings, but NATS subject tokens and KV keys accept
 8/// only a restricted character set, so ids are encoded with URL-safe Base64 (alphabet
 9/// <c>A–Z a–z 0–9 - _</c>, no padding) — every character of which is legal in both a subject token
 10/// and a KV key. Subject/key shapes are a storage contract: changing them orphans in-flight recovery
 11/// state.
 12/// </summary>
 50713internal sealed class NatsSubjectSchema(string _subjectPrefix)
 14{
 15    /// <summary>The response subject a waiter subscribes to and a publisher requests on.</summary>
 108116    public string ResponseSubject(string correlationId) => $"{_subjectPrefix}.response.{Encode(correlationId)}";
 17
 18    /// <summary>The Key-Value key under which a correlation id's recovery state is stored.</summary>
 93419    public static string RecoveryKey(string correlationId) => Encode(correlationId);
 20
 21    /// <summary>
 22    /// Recovers the original correlation id from a recovery Key-Value key. Returns the key verbatim
 23    /// when it is not valid encoded content (e.g. a key written by an older/foreign producer).
 24    /// </summary>
 2625    public static string CorrelationIdFromRecoveryKey(string recoveryKey) => Decode(recoveryKey) ?? recoveryKey;
 26
 27    /// <summary>
 28    /// UTF-8 that FAILS on ill-formed input instead of substituting U+FFFD, in both directions.
 29    /// The default encoder's substitution is a correctness problem at this particular boundary: the
 30    /// bytes it produces are the subject a waiter subscribes to and the key its recovery state is
 31    /// stored under, so two ids the engine considers different — an unpaired surrogate and a
 32    /// literal U+FFFD — would share one subject and one key, and one conversation's response would
 33    /// reach the other's waiter. Validation rejects such ids at the public boundary; this makes the
 34    /// collision unreachable from anywhere else, including ids read back from an older store.
 35    /// </summary>
 1336    private static readonly UTF8Encoding StrictUtf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: t
 37
 38    /// <summary>
 39    /// Encodes an arbitrary string to a NATS-safe token using URL-safe Base64 without padding.
 40    /// Implemented over <see cref="Convert.ToBase64String(byte[])"/> so it works identically on every
 41    /// target framework.
 42    /// </summary>
 43    public static string Encode(string value)
 44    {
 204345        ArgumentException.ThrowIfNullOrWhiteSpace(value);
 46
 203747        var base64 = Convert.ToBase64String(StrictUtf8.GetBytes(value));
 48        // '+' and '/' are illegal in NATS subject tokens / KV keys; '=' padding is dropped.
 203549        return base64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
 50    }
 51
 52    /// <summary>Decodes a token produced by <see cref="Encode"/>, or returns <c>null</c> when it is not decodable.</sum
 53    public static string? Decode(string token)
 54    {
 4655        if (string.IsNullOrEmpty(token))
 256            return null;
 57
 4458        var base64 = token.Replace('-', '+').Replace('_', '/');
 4459        switch (base64.Length % 4)
 60        {
 3261            case 2: base64 += "=="; break;
 2862            case 3: base64 += "="; break;
 263            case 1: return null; // never produced by Encode
 64        }
 65
 66        try
 67        {
 4268            return StrictUtf8.GetString(Convert.FromBase64String(base64));
 69        }
 870        catch (Exception exception) when (exception is FormatException or DecoderFallbackException)
 71        {
 72            // Not base64, or base64 of something that is not UTF-8 — either way a key this schema
 73            // did not write. Callers fall back to treating the key verbatim.
 874            return null;
 75        }
 4276    }
 77}