diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs new file mode 100644 index 0000000..b58c881 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs @@ -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); + } + +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs index 8182cf6..bb66826 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.cs @@ -43,7 +43,7 @@ public sealed class JsApiError /// Pre-built instances for all JetStream error codes. /// Mirrors the ApiErrors map in server/jetstream_errors_generated.go. /// -public static class JsApiErrors +public static partial class JsApiErrors { public delegate object? ErrorOption(); @@ -356,14 +356,10 @@ public static class JsApiErrors /// 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); } /// @@ -371,11 +367,10 @@ public static class JsApiErrors /// 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); } /// @@ -383,20 +378,20 @@ public static class JsApiErrors /// 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 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(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; diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs new file mode 100644 index 0000000..a0603a0 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs @@ -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(); + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs index 66cc22b..bb153ad 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsTests.cs @@ -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() + .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() + .ShouldBe(["{string}", "value", "{error}", "boom", "{number}", "42"]); + } } diff --git a/porting.db b/porting.db index 42fb94b..81e4bc6 100644 Binary files a/porting.db and b/porting.db differ diff --git a/tools/generate-jetstream-errors.sh b/tools/generate-jetstream-errors.sh new file mode 100755 index 0000000..9e76d78 --- /dev/null +++ b/tools/generate-jetstream-errors.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +output_file="$repo_root/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs" + +simple_methods=( + "NewJSAccountResourcesExceededError|AccountResourcesExceeded" + "NewJSAtomicPublishContainsDuplicateMessageError|AtomicPublishContainsDuplicateMessage" + "NewJSAtomicPublishDisabledError|AtomicPublishDisabled" + "NewJSAtomicPublishIncompleteBatchError|AtomicPublishIncompleteBatch" + "NewJSAtomicPublishInvalidBatchCommitError|AtomicPublishInvalidBatchCommit" + "NewJSAtomicPublishInvalidBatchIDError|AtomicPublishInvalidBatchID" + "NewJSAtomicPublishMissingSeqError|AtomicPublishMissingSeq" + "NewJSBadRequestError|BadRequest" + "NewJSClusterIncompleteError|ClusterIncomplete" + "NewJSClusterNotActiveError|ClusterNotActive" + "NewJSClusterNotAssignedError|ClusterNotAssigned" + "NewJSClusterNotAvailError|ClusterNotAvail" + "NewJSClusterNotLeaderError|ClusterNotLeader" + "NewJSClusterPeerNotMemberError|ClusterPeerNotMember" +) + +templated_methods=( + "NewJSAtomicPublishTooLargeBatchError|object?|size|AtomicPublishTooLargeBatch|{size}" + "NewJSAtomicPublishUnsupportedHeaderBatchError|object?|header|AtomicPublishUnsupportedHeaderBatch|{header}" + "NewJSClusterNoPeersError|Exception|err|ClusterNoPeers|{err}" +) + +{ + cat <<'EOF' +// 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 +{ +EOF + + for entry in "${simple_methods[@]}"; do + IFS='|' read -r method field <<<"$entry" + cat <"$output_file" + +echo "Generated $output_file"