feat(batch8): implement store enum parity group B

This commit is contained in:
Joseph Doherty
2026-02-28 11:53:44 -05:00
parent cfb49ef477
commit 59085ba9ea
4 changed files with 347 additions and 0 deletions

View File

@@ -1,4 +1,6 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
namespace ZB.MOM.NatsNet.Server;
@@ -274,3 +276,243 @@ public static class StoreParity
=> _set.Range(f);
}
}
public static class StoreEnumParityExtensions
{
public static string String(this RetentionPolicy value)
=> value switch
{
RetentionPolicy.LimitsPolicy => "Limits",
RetentionPolicy.InterestPolicy => "Interest",
RetentionPolicy.WorkQueuePolicy => "WorkQueue",
_ => "Unknown Retention Policy",
};
public static string String(this DiscardPolicy value)
=> value switch
{
DiscardPolicy.DiscardOld => "DiscardOld",
DiscardPolicy.DiscardNew => "DiscardNew",
_ => "Unknown Discard Policy",
};
public static string String(this StorageType value)
=> value switch
{
StorageType.MemoryStorage => "Memory",
StorageType.FileStorage => "File",
_ => "Unknown Storage Type",
};
}
public sealed class RetentionPolicyJsonConverter : JsonConverter<RetentionPolicy>
{
public override RetentionPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
return reader.GetString() switch
{
"limits" => RetentionPolicy.LimitsPolicy,
"interest" => RetentionPolicy.InterestPolicy,
"workqueue" => RetentionPolicy.WorkQueuePolicy,
var value => throw new JsonException($"can not unmarshal \"{value}\""),
};
}
public override void Write(Utf8JsonWriter writer, RetentionPolicy value, JsonSerializerOptions options)
{
switch (value)
{
case RetentionPolicy.LimitsPolicy:
writer.WriteStringValue("limits");
break;
case RetentionPolicy.InterestPolicy:
writer.WriteStringValue("interest");
break;
case RetentionPolicy.WorkQueuePolicy:
writer.WriteStringValue("workqueue");
break;
default:
throw new JsonException($"can not marshal {value}");
}
}
}
public sealed class DiscardPolicyJsonConverter : JsonConverter<DiscardPolicy>
{
public override DiscardPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
var token = reader.GetString() ?? string.Empty;
return token.ToLowerInvariant() switch
{
"old" => DiscardPolicy.DiscardOld,
"new" => DiscardPolicy.DiscardNew,
_ => throw new JsonException($"can not unmarshal \"{token}\""),
};
}
public override void Write(Utf8JsonWriter writer, DiscardPolicy value, JsonSerializerOptions options)
{
switch (value)
{
case DiscardPolicy.DiscardOld:
writer.WriteStringValue("old");
break;
case DiscardPolicy.DiscardNew:
writer.WriteStringValue("new");
break;
default:
throw new JsonException($"can not marshal {value}");
}
}
}
public sealed class StorageTypeJsonConverter : JsonConverter<StorageType>
{
public override StorageType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
return reader.GetString() switch
{
"memory" => StorageType.MemoryStorage,
"file" => StorageType.FileStorage,
var value => throw new JsonException($"can not unmarshal \"{value}\""),
};
}
public override void Write(Utf8JsonWriter writer, StorageType value, JsonSerializerOptions options)
{
switch (value)
{
case StorageType.MemoryStorage:
writer.WriteStringValue("memory");
break;
case StorageType.FileStorage:
writer.WriteStringValue("file");
break;
default:
throw new JsonException($"can not marshal {value}");
}
}
}
public sealed class AckPolicyJsonConverter : JsonConverter<AckPolicy>
{
public override AckPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
return reader.GetString() switch
{
"none" => AckPolicy.AckNone,
"all" => AckPolicy.AckAll,
"explicit" => AckPolicy.AckExplicit,
var value => throw new JsonException($"can not unmarshal \"{value}\""),
};
}
public override void Write(Utf8JsonWriter writer, AckPolicy value, JsonSerializerOptions options)
{
switch (value)
{
case AckPolicy.AckNone:
writer.WriteStringValue("none");
break;
case AckPolicy.AckAll:
writer.WriteStringValue("all");
break;
case AckPolicy.AckExplicit:
writer.WriteStringValue("explicit");
break;
default:
throw new JsonException($"can not marshal {value}");
}
}
}
public sealed class ReplayPolicyJsonConverter : JsonConverter<ReplayPolicy>
{
public override ReplayPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
return reader.GetString() switch
{
"instant" => ReplayPolicy.ReplayInstant,
"original" => ReplayPolicy.ReplayOriginal,
var value => throw new JsonException($"can not unmarshal \"{value}\""),
};
}
public override void Write(Utf8JsonWriter writer, ReplayPolicy value, JsonSerializerOptions options)
{
switch (value)
{
case ReplayPolicy.ReplayInstant:
writer.WriteStringValue("instant");
break;
case ReplayPolicy.ReplayOriginal:
writer.WriteStringValue("original");
break;
default:
throw new JsonException($"can not marshal {value}");
}
}
}
public sealed class DeliverPolicyJsonConverter : JsonConverter<DeliverPolicy>
{
public override DeliverPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("can not unmarshal token");
return reader.GetString() switch
{
"all" or "undefined" => DeliverPolicy.DeliverAll,
"last" => DeliverPolicy.DeliverLast,
"last_per_subject" => DeliverPolicy.DeliverLastPerSubject,
"new" => DeliverPolicy.DeliverNew,
"by_start_sequence" => DeliverPolicy.DeliverByStartSequence,
"by_start_time" => DeliverPolicy.DeliverByStartTime,
var value => throw new JsonException($"can not unmarshal \"{value}\""),
};
}
public override void Write(Utf8JsonWriter writer, DeliverPolicy value, JsonSerializerOptions options)
{
switch (value)
{
case DeliverPolicy.DeliverAll:
writer.WriteStringValue("all");
break;
case DeliverPolicy.DeliverLast:
writer.WriteStringValue("last");
break;
case DeliverPolicy.DeliverLastPerSubject:
writer.WriteStringValue("last_per_subject");
break;
case DeliverPolicy.DeliverNew:
writer.WriteStringValue("new");
break;
case DeliverPolicy.DeliverByStartSequence:
writer.WriteStringValue("by_start_sequence");
break;
case DeliverPolicy.DeliverByStartTime:
writer.WriteStringValue("by_start_time");
break;
default:
writer.WriteStringValue("undefined");
break;
}
}
}

View File

@@ -23,6 +23,7 @@ namespace ZB.MOM.NatsNet.Server;
// ---------------------------------------------------------------------------
/// <summary>Determines how messages are stored for retention.</summary>
[JsonConverter(typeof(StorageTypeJsonConverter))]
public enum StorageType
{
/// <summary>On disk, designated by the JetStream config StoreDir.</summary>
@@ -228,6 +229,7 @@ public interface IStreamStore
// ---------------------------------------------------------------------------
/// <summary>Determines how messages in a stream are retained.</summary>
[JsonConverter(typeof(RetentionPolicyJsonConverter))]
public enum RetentionPolicy
{
/// <summary>Messages are retained until any given limit is reached.</summary>
@@ -245,6 +247,7 @@ public enum RetentionPolicy
// ---------------------------------------------------------------------------
/// <summary>Determines how the store proceeds when message or byte limits are hit.</summary>
[JsonConverter(typeof(DiscardPolicyJsonConverter))]
public enum DiscardPolicy
{
/// <summary>Remove older messages to return to the limits.</summary>
@@ -542,6 +545,7 @@ public sealed class ConsumerState
// ---------------------------------------------------------------------------
/// <summary>Determines how the consumer should acknowledge delivered messages.</summary>
[JsonConverter(typeof(AckPolicyJsonConverter))]
public enum AckPolicy
{
/// <summary>No acks required for delivered messages.</summary>
@@ -559,6 +563,7 @@ public enum AckPolicy
// ---------------------------------------------------------------------------
/// <summary>Determines how the consumer replays messages already queued in the stream.</summary>
[JsonConverter(typeof(ReplayPolicyJsonConverter))]
public enum ReplayPolicy
{
/// <summary>Replay messages as fast as possible.</summary>
@@ -573,6 +578,7 @@ public enum ReplayPolicy
// ---------------------------------------------------------------------------
/// <summary>Determines how the consumer selects the first message to deliver.</summary>
[JsonConverter(typeof(DeliverPolicyJsonConverter))]
public enum DeliverPolicy
{
/// <summary>Deliver all messages (default).</summary>

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;
@@ -223,6 +224,104 @@ public class StoreTypesTests
StoreParity.IsPermissionError(null).ShouldBeFalse();
}
[Fact]
public void RetentionPolicy_StringParity_ReturnsExpectedText()
{
RetentionPolicy.LimitsPolicy.String().ShouldBe("Limits");
RetentionPolicy.InterestPolicy.String().ShouldBe("Interest");
RetentionPolicy.WorkQueuePolicy.String().ShouldBe("WorkQueue");
((RetentionPolicy)99).String().ShouldBe("Unknown Retention Policy");
}
[Fact]
public void RetentionPolicy_JsonParity_RoundTripsExpectedTokens()
{
JsonSerializer.Serialize(RetentionPolicy.LimitsPolicy).ShouldBe("\"limits\"");
JsonSerializer.Serialize(RetentionPolicy.InterestPolicy).ShouldBe("\"interest\"");
JsonSerializer.Serialize(RetentionPolicy.WorkQueuePolicy).ShouldBe("\"workqueue\"");
JsonSerializer.Deserialize<RetentionPolicy>("\"limits\"").ShouldBe(RetentionPolicy.LimitsPolicy);
JsonSerializer.Deserialize<RetentionPolicy>("\"interest\"").ShouldBe(RetentionPolicy.InterestPolicy);
JsonSerializer.Deserialize<RetentionPolicy>("\"workqueue\"").ShouldBe(RetentionPolicy.WorkQueuePolicy);
}
[Fact]
public void RetentionPolicy_UnmarshalInvalid_Throws()
{
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<RetentionPolicy>("\"bogus\""));
}
[Fact]
public void DiscardPolicy_StringAndJsonParity_MatchesGo()
{
DiscardPolicy.DiscardOld.String().ShouldBe("DiscardOld");
DiscardPolicy.DiscardNew.String().ShouldBe("DiscardNew");
((DiscardPolicy)99).String().ShouldBe("Unknown Discard Policy");
JsonSerializer.Serialize(DiscardPolicy.DiscardOld).ShouldBe("\"old\"");
JsonSerializer.Serialize(DiscardPolicy.DiscardNew).ShouldBe("\"new\"");
JsonSerializer.Deserialize<DiscardPolicy>("\"OLD\"").ShouldBe(DiscardPolicy.DiscardOld);
JsonSerializer.Deserialize<DiscardPolicy>("\"new\"").ShouldBe(DiscardPolicy.DiscardNew);
}
[Fact]
public void StorageType_StringAndJsonParity_MatchesGo()
{
StorageType.MemoryStorage.String().ShouldBe("Memory");
StorageType.FileStorage.String().ShouldBe("File");
((StorageType)99).String().ShouldBe("Unknown Storage Type");
JsonSerializer.Serialize(StorageType.MemoryStorage).ShouldBe("\"memory\"");
JsonSerializer.Serialize(StorageType.FileStorage).ShouldBe("\"file\"");
JsonSerializer.Deserialize<StorageType>("\"memory\"").ShouldBe(StorageType.MemoryStorage);
JsonSerializer.Deserialize<StorageType>("\"file\"").ShouldBe(StorageType.FileStorage);
}
[Fact]
public void AckPolicy_JsonParity_MatchesGo()
{
JsonSerializer.Serialize(AckPolicy.AckNone).ShouldBe("\"none\"");
JsonSerializer.Serialize(AckPolicy.AckAll).ShouldBe("\"all\"");
JsonSerializer.Serialize(AckPolicy.AckExplicit).ShouldBe("\"explicit\"");
JsonSerializer.Deserialize<AckPolicy>("\"none\"").ShouldBe(AckPolicy.AckNone);
JsonSerializer.Deserialize<AckPolicy>("\"all\"").ShouldBe(AckPolicy.AckAll);
JsonSerializer.Deserialize<AckPolicy>("\"explicit\"").ShouldBe(AckPolicy.AckExplicit);
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<AckPolicy>("\"bad\""));
}
[Fact]
public void ReplayPolicy_JsonParity_MatchesGo()
{
JsonSerializer.Serialize(ReplayPolicy.ReplayInstant).ShouldBe("\"instant\"");
JsonSerializer.Serialize(ReplayPolicy.ReplayOriginal).ShouldBe("\"original\"");
JsonSerializer.Deserialize<ReplayPolicy>("\"instant\"").ShouldBe(ReplayPolicy.ReplayInstant);
JsonSerializer.Deserialize<ReplayPolicy>("\"original\"").ShouldBe(ReplayPolicy.ReplayOriginal);
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<ReplayPolicy>("\"bad\""));
}
[Fact]
public void DeliverPolicy_JsonParity_MapsUndefinedToAll()
{
JsonSerializer.Serialize(DeliverPolicy.DeliverAll).ShouldBe("\"all\"");
JsonSerializer.Serialize(DeliverPolicy.DeliverLast).ShouldBe("\"last\"");
JsonSerializer.Serialize(DeliverPolicy.DeliverLastPerSubject).ShouldBe("\"last_per_subject\"");
JsonSerializer.Serialize(DeliverPolicy.DeliverNew).ShouldBe("\"new\"");
JsonSerializer.Serialize(DeliverPolicy.DeliverByStartSequence).ShouldBe("\"by_start_sequence\"");
JsonSerializer.Serialize(DeliverPolicy.DeliverByStartTime).ShouldBe("\"by_start_time\"");
JsonSerializer.Serialize((DeliverPolicy)99).ShouldBe("\"undefined\"");
JsonSerializer.Deserialize<DeliverPolicy>("\"all\"").ShouldBe(DeliverPolicy.DeliverAll);
JsonSerializer.Deserialize<DeliverPolicy>("\"undefined\"").ShouldBe(DeliverPolicy.DeliverAll);
JsonSerializer.Deserialize<DeliverPolicy>("\"last\"").ShouldBe(DeliverPolicy.DeliverLast);
JsonSerializer.Deserialize<DeliverPolicy>("\"last_per_subject\"").ShouldBe(DeliverPolicy.DeliverLastPerSubject);
JsonSerializer.Deserialize<DeliverPolicy>("\"new\"").ShouldBe(DeliverPolicy.DeliverNew);
JsonSerializer.Deserialize<DeliverPolicy>("\"by_start_sequence\"").ShouldBe(DeliverPolicy.DeliverByStartSequence);
JsonSerializer.Deserialize<DeliverPolicy>("\"by_start_time\"").ShouldBe(DeliverPolicy.DeliverByStartTime);
Should.Throw<JsonException>(() => JsonSerializer.Deserialize<DeliverPolicy>("\"bad\""));
}
private static void AppendUVarInt(List<byte> buffer, ulong value)
{
while (value >= 0x80)

Binary file not shown.