From 9f30fe60331c971fa074c415d3eb2e71b4e947a2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 08:25:56 -0500 Subject: [PATCH] feat(batch5): implement jetstream error helpers and group01 constructors --- .../JetStreamErrors.GeneratedConstructors.cs | 147 ++++++++++++++++++ .../JetStream/JetStreamErrors.cs | 75 ++++++--- ...tStreamErrorsGeneratedConstructorsTests.cs | 39 +++++ .../JetStream/JetStreamErrorsTests.cs | 36 +++++ porting.db | Bin 6373376 -> 6381568 bytes tools/generate-jetstream-errors.sh | 77 +++++++++ 6 files changed, 349 insertions(+), 25 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/JetStreamErrors.GeneratedConstructors.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamErrorsGeneratedConstructorsTests.cs create mode 100755 tools/generate-jetstream-errors.sh 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 42fb94b0b4680089dc3e3096faa3a0c190d18da8..81e4bc69692833f2d6ff4885c8405a2ce13694ca 100644 GIT binary patch delta 8081 zcmchbeM}qY9mns^_WAKU5HKWQVgjLrciUjVO#(DD=~}>~O|p@u4GtWE5Nxmw+NPz% zX)>nuTBVEHXX;ih>7+HsoMikAy(GU$qzGyelwME^xMhlLpy)yF))d*I=%J?w zf?$Q}yP#eX)Fh}5K}~=1XQ-5hC!{EGYo-Tad(I7Um%Sn zMNi{9i)2+UJukX;PIT?8=+YVPnI-b(Ci2!>3hH&a;zG-t`8CZ?J3dYLAA2~?(_7C|KmY5|m5Q1hCz z#Fv7a1N9F<&4L;i)C{P<32GYD9|Sd}={ZYW7vx>2yez0mQ11w80u(E#aZs-aY7A6| zphiI*6VwQ(1A-a`wO3F>nzIy9ir=8AOgU8Oxf?XK?FUKv%hDh2(@WH!$s&45H$Yf@ z{;p&6B9yX1r3EOZhD!5LiVv0Mprj6!X1RaE|2SE%Z}Dp>**iZBD2!pEA15+5s=_k65i&hxQ?`Oe1*<~kp19KZP+ zs+Rlr6-u9m4(Um#pp95NKwZ9Y?YtZZN|K7XPX?%Xofaw*SQ~uDvnjp0Q3{X&azFvl zoNiS4h07@am+UIVkg56)b?KT9)vu{c${Tde`{X1=;=ewNhOqZ`YAUXyQ4Dt1A{viB zhw}0GQE77l1*Xz&+v8!AD+=bNm3)ixNWv6M!&<*^aL2tqp zDij+W4Z?;-l(`bphRYk#fH)Y9Pc! z$C*wbM6POxkG7$(S+#NBZ$l}MG}?L+CE%(~Woq~Ua@vF5LTeuyV6b0ocBA;!qrg9N zqd9Rydniy|aM^7-YLJK_P7#I*-M6&wYVK?7>K@fcszl@=Zb)xOz)z(gQ4ZlY#Q{VQY+)zbQ z$OGIXS0VM<&Kx|bYoo}7lJquQcmf&mQ7<})8+>RB_a`r^cx1RxZNiD$Me~fm<46Au z8}1+d&}N`Z)TXzu8v!an4QK!@pab+=U$^0fZ=i>imNI;5m6OamQ1T6amG69Vh`xfil1WJPGUsb^*JAa-agJ1fBx+09C-#z+PY<@C@)3;91}~pc<$F zY5^xu2kZx)2VMXU00)6Xz*m8<0rkLP;6>mF@DlJcaFiSVydkStmPiD!ve30dLH>oz zBHpqjS0@G1*%UQlqD;<&K#r}MarwKvjE^3|mttIbJqKLAHnYXN&+F;xFc(_Pt!{IN z*K-_N_#9@R-{tkUx{sUTz)=9dF>Z5f@QByWxcx4FtH*72xtq*vS)%_tp+`IZNsKEi8YXtJTQtE*2#}z~=n%!PVukJsz*`tlon z4qI{P0Ud>z$znIFiL}tlSh#nsu11|x2U}MnVJ8gr`d{ibx+d*!wMLCc{gFCN^-Xje zaTJoack~>+el%0_X?O4jzvQrU(9gCD5Y`J$NdHs`Pv8?50{gdNh3zt(-v{pm> ztKMxMJK&od5CX zjTvY1F&S+W+ayd(MB7A~gbDutv8bI? z(^<2GiLq$U#8wFtqtP~zDq&(I+9pgACWfPGBKT6Emv<6|JpEx^ulCoPagA18rfNpt zQ~r)tF43D6Ps=+w_yey-zBy3GR`O@9JkjxTY1P@g>@lwb7EAG3XR-Akck9>+DL;#m z`YGI?v#66T=lzIZl@?a{dEBc~*_!Vr>z^JcyIac6e57_Z=;g%8?vk=I7onX}+gi!9 z{x_C7cBfSG;L5SXVQ+4a&1Kt$E!LA#vojOn?38TKt>t7LQg)^zv}0fM&WdEQ$|M&n zt@*`>7Hfl2xsENBvU7LU>^wGktZU|SBzJm=)M`zx^7FWjr+CA1*)C;gB2qgWv|3KK eSjx_Lgm#KHY_W=@?2JWd$F}C3p0(b8u>J?b8}NYu delta 1829 zcmY+^e{54#6bJBkZ`;??_xk$ER!U(%*amDc1|1Uy1A%UA6=aNUf+9@VV8xAX&<&U% z3flyS{E%Y8PK+@DHDZuVhQuo}l7+tviJ}RAm>3rZLx~^)Di{cfr+xHM(|nTiz2}{K z-+S%WF|fzd(Xq!8NwJkklI%I0F`t$6)du`*^jpXL;+UU3?!z-fYz|%;VqWp*5L?P~ ztw$x-x=k(N=FpaqvR|{L@pxi85__x+8-`ge2Zy*g%o=RF)ICnObRd4Q+C^!nnc69Z z&9o|UTV9r!oYjd5boHV%D?SG7%O;=VpEF<~mEnC+r%gn06tjf)5u~{t= ze|^laJLWm7<4d5sYNUebCg7rKVyx1rN{>}4Ri0R-P^H8w2YP#XdC+(;Z5zF`dGW{8 zUZa$6rglnMW~zJo@bS}m86N(ISBU$)+?5H-TpA6?9(4Y|Kf~hLz9R8QrJkX6YlECv zh?UrgEVM!UqdKqaxVF~mQ+{+Dvi~g~vej5_^LJSKadwq6u_OpzIxxK$e#QbBreaPB zOvk`Ehw2C(Y)X}3t_26~TmUNelt7L6J$Z0sR#jfHftuVoOBbQs@3v9a%>3DR%UBS=WASV{bp7fJ6N}71i@Rh%M*&@$MTBb>b!? zND@gV9x{@6$tdC@qsbVOLQ+W@Nhf1T2FWB@WE>e!CXj5BLne|*B$rGkd1MNiO8g|B zOd|#488V&BAcbTmnMI1oY%+&DOXiY!WIhRyVp2jDkWx}cg5)`}kSrq4lf|T*EFl$S zDXAn?q?)`ymXYP;Me-6^L0%>+$tqGqULmW=8gb)cZR!k*heyz!UpEP}I-CoI`zrTw K5h"$output_file" + +echo "Generated $output_file"