feat(batch5): implement jetstream error helpers and group01 constructors

This commit is contained in:
Joseph Doherty
2026-02-28 08:25:56 -05:00
parent 9fa81d472e
commit 9f30fe6033
6 changed files with 349 additions and 25 deletions

View File

@@ -0,0 +1,147 @@
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
//
// Generated constructor surface for JetStream API errors.
// Source parity: server/jetstream_errors_generated.go
namespace ZB.MOM.NatsNet.Server;
public static partial class JsApiErrors
{
public static JsApiError NewJSAccountResourcesExceededError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AccountResourcesExceeded);
}
public static JsApiError NewJSAtomicPublishContainsDuplicateMessageError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishContainsDuplicateMessage);
}
public static JsApiError NewJSAtomicPublishDisabledError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishDisabled);
}
public static JsApiError NewJSAtomicPublishIncompleteBatchError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishIncompleteBatch);
}
public static JsApiError NewJSAtomicPublishInvalidBatchCommitError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishInvalidBatchCommit);
}
public static JsApiError NewJSAtomicPublishInvalidBatchIDError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishInvalidBatchID);
}
public static JsApiError NewJSAtomicPublishMissingSeqError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(AtomicPublishMissingSeq);
}
public static JsApiError NewJSBadRequestError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(BadRequest);
}
public static JsApiError NewJSClusterIncompleteError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterIncomplete);
}
public static JsApiError NewJSClusterNotActiveError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterNotActive);
}
public static JsApiError NewJSClusterNotAssignedError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterNotAssigned);
}
public static JsApiError NewJSClusterNotAvailError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterNotAvail);
}
public static JsApiError NewJSClusterNotLeaderError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterNotLeader);
}
public static JsApiError NewJSClusterPeerNotMemberError(params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(ClusterPeerNotMember);
}
public static JsApiError NewJSAtomicPublishTooLargeBatchError(object? size, params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return NewWithTags(AtomicPublishTooLargeBatch, "{size}", size);
}
public static JsApiError NewJSAtomicPublishUnsupportedHeaderBatchError(object? header, params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return NewWithTags(AtomicPublishUnsupportedHeaderBatch, "{header}", header);
}
public static JsApiError NewJSClusterNoPeersError(Exception err, params ErrorOption[] opts)
{
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return NewWithTags(ClusterNoPeers, "{err}", err);
}
}

View File

@@ -43,7 +43,7 @@ public sealed class JsApiError
/// Pre-built <see cref="JsApiError"/> instances for all JetStream error codes.
/// Mirrors the <c>ApiErrors</c> map in server/jetstream_errors_generated.go.
/// </summary>
public static class JsApiErrors
public static partial class JsApiErrors
{
public delegate object? ErrorOption();
@@ -356,14 +356,10 @@ public static class JsApiErrors
/// </summary>
public static JsApiError NewJSRestoreSubscribeFailedError(Exception err, string subject, params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
if (overridden != null)
return overridden;
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return NewWithTags(
RestoreSubscribeFailed,
("{err}", err.Message),
("{subject}", subject));
return NewWithTags(RestoreSubscribeFailed, "{subject}", subject, "{err}", err);
}
/// <summary>
@@ -371,11 +367,10 @@ public static class JsApiErrors
/// </summary>
public static JsApiError NewJSStreamRestoreError(Exception err, params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
if (overridden != null)
return overridden;
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return NewWithTags(StreamRestore, ("{err}", err.Message));
return NewWithTags(StreamRestore, "{err}", err);
}
/// <summary>
@@ -383,20 +378,20 @@ public static class JsApiErrors
/// </summary>
public static JsApiError NewJSPeerRemapError(params ErrorOption[] opts)
{
var overridden = ParseUnless(opts);
return overridden ?? Clone(PeerRemap);
if (ParseOpts(opts) is JsApiError overridden)
return Clone(overridden);
return Clone(PeerRemap);
}
private static JsApiError? ParseUnless(ReadOnlySpan<ErrorOption> opts)
private static object? ParseOpts(params ErrorOption[] opts)
{
foreach (var opt in opts)
{
var value = opt();
if (value is JsApiError apiErr)
return Clone(apiErr);
}
object? value = null;
return null;
foreach (var opt in opts)
value = opt();
return value;
}
private static JsApiError Clone(JsApiError source) => new()
@@ -406,13 +401,43 @@ public static class JsApiErrors
Description = source.Description,
};
private static JsApiError NewWithTags(JsApiError source, params (string key, string value)[] replacements)
private static string[] ToReplacerArgs(params object?[] replacements)
{
var args = new List<string>(replacements.Length);
string key = string.Empty;
for (var i = 0; i < replacements.Length; i++)
{
if (i % 2 == 0)
{
key = replacements[i] as string
?? throw new InvalidOperationException("Replacement keys must be strings.");
continue;
}
var value = replacements[i] switch
{
string s => s,
Exception ex => ex.Message,
null => string.Empty,
var other => other.ToString() ?? string.Empty,
};
args.Add(key);
args.Add(value);
}
return args.ToArray();
}
private static JsApiError NewWithTags(JsApiError source, params object?[] replacements)
{
var clone = Clone(source);
var description = clone.Description ?? string.Empty;
var args = ToReplacerArgs(replacements);
foreach (var (key, value) in replacements)
description = description.Replace(key, value, StringComparison.Ordinal);
for (var i = 0; i + 1 < args.Length; i += 2)
description = description.Replace(args[i], args[i + 1], StringComparison.Ordinal);
clone.Description = description;
return clone;

View File

@@ -0,0 +1,39 @@
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
using Shouldly;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class JetStreamErrorsGeneratedConstructorsTests
{
[Fact]
public void ConstructorSurface_Group01()
{
JsApiErrors.NewJSAccountResourcesExceededError().ErrCode.ShouldBe(JsApiErrors.AccountResourcesExceeded.ErrCode);
JsApiErrors.NewJSAtomicPublishContainsDuplicateMessageError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishContainsDuplicateMessage.ErrCode);
JsApiErrors.NewJSAtomicPublishDisabledError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishDisabled.ErrCode);
JsApiErrors.NewJSAtomicPublishIncompleteBatchError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishIncompleteBatch.ErrCode);
JsApiErrors.NewJSAtomicPublishInvalidBatchCommitError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishInvalidBatchCommit.ErrCode);
JsApiErrors.NewJSAtomicPublishInvalidBatchIDError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishInvalidBatchID.ErrCode);
JsApiErrors.NewJSAtomicPublishMissingSeqError().ErrCode.ShouldBe(JsApiErrors.AtomicPublishMissingSeq.ErrCode);
JsApiErrors.NewJSBadRequestError().ErrCode.ShouldBe(JsApiErrors.BadRequest.ErrCode);
JsApiErrors.NewJSClusterIncompleteError().ErrCode.ShouldBe(JsApiErrors.ClusterIncomplete.ErrCode);
JsApiErrors.NewJSClusterNotActiveError().ErrCode.ShouldBe(JsApiErrors.ClusterNotActive.ErrCode);
JsApiErrors.NewJSClusterNotAssignedError().ErrCode.ShouldBe(JsApiErrors.ClusterNotAssigned.ErrCode);
JsApiErrors.NewJSClusterNotAvailError().ErrCode.ShouldBe(JsApiErrors.ClusterNotAvail.ErrCode);
JsApiErrors.NewJSClusterNotLeaderError().ErrCode.ShouldBe(JsApiErrors.ClusterNotLeader.ErrCode);
JsApiErrors.NewJSClusterPeerNotMemberError().ErrCode.ShouldBe(JsApiErrors.ClusterPeerNotMember.ErrCode);
JsApiErrors.NewJSAtomicPublishTooLargeBatchError(512).Description.ShouldBe("atomic publish batch is too large: 512");
JsApiErrors.NewJSAtomicPublishUnsupportedHeaderBatchError("Nats-Msg-Id").Description.ShouldBe("atomic publish unsupported header used: Nats-Msg-Id");
JsApiErrors.NewJSClusterNoPeersError(new InvalidOperationException("no peers")).Description.ShouldBe("no peers");
var expected = new JsApiError { Code = 499, ErrCode = 9090, Description = "override" };
var fromOverride = JsApiErrors.NewJSAccountResourcesExceededError(JsApiErrors.Unless(expected));
fromOverride.Code.ShouldBe(expected.Code);
fromOverride.ErrCode.ShouldBe(expected.ErrCode);
fromOverride.Description.ShouldBe(expected.Description);
ReferenceEquals(fromOverride, expected).ShouldBeFalse();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
using System.Reflection;
using Shouldly;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
@@ -97,4 +98,39 @@ public sealed class JetStreamErrorsTests
JsApiErrors.NewJSPeerRemapError(JsApiErrors.Unless(new Exception("other error"))),
peerRemap).ShouldBeTrue();
}
[Fact]
public void ParseOpts_WithUnlessApiErrorOption_ReturnsOverride()
{
var parseOpts = typeof(JsApiErrors).GetMethod(
"ParseOpts",
BindingFlags.NonPublic | BindingFlags.Static);
parseOpts.ShouldNotBeNull();
var expected = new JsApiError { Code = 401, ErrCode = 2048, Description = "override" };
var result = parseOpts.Invoke(
null,
[new JsApiErrors.ErrorOption[] { JsApiErrors.Unless(expected) }]);
result.ShouldBeAssignableTo<JsApiError>()
.ErrCode.ShouldBe(expected.ErrCode);
}
[Fact]
public void ToReplacerArgs_WithStringErrorAndObject_ConvertsToStringValues()
{
var toReplacerArgs = typeof(JsApiErrors).GetMethod(
"ToReplacerArgs",
BindingFlags.NonPublic | BindingFlags.Static);
toReplacerArgs.ShouldNotBeNull();
var result = toReplacerArgs.Invoke(
null,
[new object?[] { "{string}", "value", "{error}", new InvalidOperationException("boom"), "{number}", 42 }]);
result.ShouldBeAssignableTo<string[]>()
.ShouldBe(["{string}", "value", "{error}", "boom", "{number}", "42"]);
}
}