feat: add gateway command protocol with Go-compatible wire format (Gap 11.6)
Add GatewayCommands static class with wire-format byte sequences (GINFO, GS+, GS-, GMODE, GMSG, GPING, GPONG) and FormatSub/FormatUnsub/FormatMode/ParseCommandType helpers matching Go's gateway.go protocol constants. Add GatewayCommandType enum. 10 tests covering all wire formats and command parsing.
This commit is contained in:
87
src/NATS.Server/Gateways/GatewayCommands.cs
Normal file
87
src/NATS.Server/Gateways/GatewayCommands.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.Text;
|
||||
|
||||
namespace NATS.Server.Gateways;
|
||||
|
||||
/// <summary>
|
||||
/// Gateway wire protocol command types matching Go's gateway.go format.
|
||||
/// Go reference: gateway.go:90-120 — gateway protocol constants.
|
||||
/// </summary>
|
||||
public enum GatewayCommandType
|
||||
{
|
||||
/// <summary>Gateway connection info exchange.</summary>
|
||||
Info,
|
||||
/// <summary>Subscribe interest notification.</summary>
|
||||
Sub,
|
||||
/// <summary>Unsubscribe interest notification.</summary>
|
||||
Unsub,
|
||||
/// <summary>Account interest mode change.</summary>
|
||||
Mode,
|
||||
/// <summary>Routed message through gateway.</summary>
|
||||
Msg,
|
||||
/// <summary>Ping keepalive.</summary>
|
||||
Ping,
|
||||
/// <summary>Pong keepalive response.</summary>
|
||||
Pong,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gateway wire protocol byte sequences and formatting helpers matching Go's gateway protocol.
|
||||
/// Go reference: gateway.go:90-120 — gateway protocol constants and command formatting.
|
||||
/// </summary>
|
||||
public static class GatewayCommands
|
||||
{
|
||||
// Wire format byte sequences matching Go's gateway protocol
|
||||
public static readonly byte[] InfoPrefix = "GINFO "u8.ToArray();
|
||||
public static readonly byte[] SubPrefix = "GS+ "u8.ToArray();
|
||||
public static readonly byte[] UnsubPrefix = "GS- "u8.ToArray();
|
||||
public static readonly byte[] ModePrefix = "GMODE "u8.ToArray();
|
||||
public static readonly byte[] MsgPrefix = "GMSG "u8.ToArray();
|
||||
public static readonly byte[] Ping = "GPING\r\n"u8.ToArray();
|
||||
public static readonly byte[] Pong = "GPONG\r\n"u8.ToArray();
|
||||
public static readonly byte[] Crlf = "\r\n"u8.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Formats a gateway subscribe command.
|
||||
/// Wire format: GS+ {account} {subject}\r\n
|
||||
/// Go reference: gateway.go — sendGatewaySubsToGateway, RS+ propagation.
|
||||
/// </summary>
|
||||
public static byte[] FormatSub(string account, string subject)
|
||||
=> Encoding.UTF8.GetBytes($"GS+ {account} {subject}\r\n");
|
||||
|
||||
/// <summary>
|
||||
/// Formats a gateway unsubscribe command.
|
||||
/// Wire format: GS- {account} {subject}\r\n
|
||||
/// Go reference: gateway.go — sendGatewayUnsubToGateway, RS- propagation.
|
||||
/// </summary>
|
||||
public static byte[] FormatUnsub(string account, string subject)
|
||||
=> Encoding.UTF8.GetBytes($"GS- {account} {subject}\r\n");
|
||||
|
||||
/// <summary>
|
||||
/// Formats a gateway mode change command.
|
||||
/// Wire format: GMODE {account} {mode}\r\n
|
||||
/// Mode: "O" for Optimistic (send everything), "I" for Interest-only.
|
||||
/// Go reference: gateway.go — switchAccountToInterestMode, GMODE command.
|
||||
/// </summary>
|
||||
public static byte[] FormatMode(string account, GatewayInterestMode mode)
|
||||
{
|
||||
var modeStr = mode == GatewayInterestMode.InterestOnly ? "I" : "O";
|
||||
return Encoding.UTF8.GetBytes($"GMODE {account} {modeStr}\r\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a gateway command type from the first bytes of a line.
|
||||
/// Returns null if the command prefix is unrecognized.
|
||||
/// Go reference: gateway.go — processGatewayMsg command dispatch.
|
||||
/// </summary>
|
||||
public static GatewayCommandType? ParseCommandType(ReadOnlySpan<byte> line)
|
||||
{
|
||||
if (line.StartsWith(InfoPrefix)) return GatewayCommandType.Info;
|
||||
if (line.StartsWith(SubPrefix)) return GatewayCommandType.Sub;
|
||||
if (line.StartsWith(UnsubPrefix)) return GatewayCommandType.Unsub;
|
||||
if (line.StartsWith(ModePrefix)) return GatewayCommandType.Mode;
|
||||
if (line.StartsWith(MsgPrefix)) return GatewayCommandType.Msg;
|
||||
if (line.StartsWith(Ping)) return GatewayCommandType.Ping;
|
||||
if (line.StartsWith(Pong)) return GatewayCommandType.Pong;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
99
tests/NATS.Server.Tests/Gateways/GatewayCommandTests.cs
Normal file
99
tests/NATS.Server.Tests/Gateways/GatewayCommandTests.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Text;
|
||||
using NATS.Server.Gateways;
|
||||
using Shouldly;
|
||||
|
||||
namespace NATS.Server.Tests.Gateways;
|
||||
|
||||
// Go reference: gateway.go:90-120 — gateway protocol constants and command formatting.
|
||||
|
||||
public class GatewayCommandTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormatSub_produces_correct_wire_format()
|
||||
{
|
||||
// Go reference: gateway.go — RS+ propagation, sendGatewaySubsToGateway
|
||||
var bytes = GatewayCommands.FormatSub("$G", "orders.>");
|
||||
var line = Encoding.UTF8.GetString(bytes);
|
||||
line.ShouldBe("GS+ $G orders.>\r\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatUnsub_produces_correct_wire_format()
|
||||
{
|
||||
// Go reference: gateway.go — RS- propagation, sendGatewayUnsubToGateway
|
||||
var bytes = GatewayCommands.FormatUnsub("$G", "orders.>");
|
||||
var line = Encoding.UTF8.GetString(bytes);
|
||||
line.ShouldBe("GS- $G orders.>\r\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMode_optimistic_produces_O()
|
||||
{
|
||||
// Go reference: gateway.go — GMODE command, "O" = Optimistic mode
|
||||
var bytes = GatewayCommands.FormatMode("$G", GatewayInterestMode.Optimistic);
|
||||
var line = Encoding.UTF8.GetString(bytes);
|
||||
line.ShouldBe("GMODE $G O\r\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMode_interest_only_produces_I()
|
||||
{
|
||||
// Go reference: gateway.go — GMODE command, "I" = InterestOnly mode
|
||||
var bytes = GatewayCommands.FormatMode("$G", GatewayInterestMode.InterestOnly);
|
||||
var line = Encoding.UTF8.GetString(bytes);
|
||||
line.ShouldBe("GMODE $G I\r\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCommandType_identifies_sub()
|
||||
{
|
||||
// Go reference: gateway.go — processGatewayMsg dispatch on GS+
|
||||
var line = Encoding.UTF8.GetBytes("GS+ ACC foo.bar\r\n");
|
||||
var result = GatewayCommands.ParseCommandType(line);
|
||||
result.ShouldBe(GatewayCommandType.Sub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCommandType_identifies_unsub()
|
||||
{
|
||||
// Go reference: gateway.go — processGatewayMsg dispatch on GS-
|
||||
var line = Encoding.UTF8.GetBytes("GS- ACC foo.bar\r\n");
|
||||
var result = GatewayCommands.ParseCommandType(line);
|
||||
result.ShouldBe(GatewayCommandType.Unsub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCommandType_identifies_ping()
|
||||
{
|
||||
// Go reference: gateway.go — keepalive GPING command
|
||||
var result = GatewayCommands.ParseCommandType(GatewayCommands.Ping);
|
||||
result.ShouldBe(GatewayCommandType.Ping);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCommandType_identifies_pong()
|
||||
{
|
||||
// Go reference: gateway.go — keepalive GPONG response
|
||||
var result = GatewayCommands.ParseCommandType(GatewayCommands.Pong);
|
||||
result.ShouldBe(GatewayCommandType.Pong);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseCommandType_returns_null_for_unknown()
|
||||
{
|
||||
// Unrecognized commands should return null for graceful handling
|
||||
var line = Encoding.UTF8.GetBytes("UNKNOWN something\r\n");
|
||||
var result = GatewayCommands.ParseCommandType(line);
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wire_format_constants_end_correctly()
|
||||
{
|
||||
// Go reference: gateway.go — all gateway commands use CRLF line endings
|
||||
var crlf = new byte[] { (byte)'\r', (byte)'\n' };
|
||||
GatewayCommands.Ping.TakeLast(2).ShouldBe(crlf);
|
||||
GatewayCommands.Pong.TakeLast(2).ShouldBe(crlf);
|
||||
GatewayCommands.Crlf.ShouldBe(crlf);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user