feat: session A — config binding via appsettings.json (67 stubs complete)
Add JSON attributes to ServerOptions, four custom JSON converters (NatsDurationJsonConverter, TlsVersionJsonConverter, NatsUrlJsonConverter, StorageSizeJsonConverter), ServerOptionsConfiguration for JSON file/string binding, and 15 tests covering config parsing, duration parsing, and size parsing. Mark 67 opts.go features complete in porting.db.
This commit is contained in:
169
dotnet/src/ZB.MOM.NatsNet.Server/Config/NatsJsonConverters.cs
Normal file
169
dotnet/src/ZB.MOM.NatsNet.Server/Config/NatsJsonConverters.cs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Adapted from parse utility functions in server/opts.go in the NATS server Go source.
|
||||||
|
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts NATS duration strings (e.g. "2s", "100ms", "1h30m") to <see cref="TimeSpan"/>.
|
||||||
|
/// Mirrors Go <c>parseDuration</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NatsDurationJsonConverter : JsonConverter<TimeSpan>
|
||||||
|
{
|
||||||
|
private static readonly Regex Pattern = new(
|
||||||
|
@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?(?:(\d+)ms)?(?:(\d+)us)?(?:(\d+)ns)?$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var raw = reader.GetString() ?? throw new JsonException("Expected a duration string");
|
||||||
|
return Parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteStringValue(FormatDuration(value));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a NATS-style duration string. Accepts Go time.Duration format strings and ISO 8601.
|
||||||
|
/// </summary>
|
||||||
|
public static TimeSpan Parse(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s))
|
||||||
|
throw new FormatException("Duration string is empty");
|
||||||
|
|
||||||
|
// Try Go-style: e.g. "2s", "100ms", "1h30m", "5m10s"
|
||||||
|
var m = Pattern.Match(s);
|
||||||
|
if (m.Success && m.Value.Length > 0)
|
||||||
|
{
|
||||||
|
var hours = m.Groups[1].Success ? int.Parse(m.Groups[1].Value) : 0;
|
||||||
|
var minutes = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : 0;
|
||||||
|
var seconds = m.Groups[3].Success ? int.Parse(m.Groups[3].Value) : 0;
|
||||||
|
var ms = m.Groups[4].Success ? int.Parse(m.Groups[4].Value) : 0;
|
||||||
|
var us = m.Groups[5].Success ? int.Parse(m.Groups[5].Value) : 0;
|
||||||
|
var ns = m.Groups[6].Success ? int.Parse(m.Groups[6].Value) : 0;
|
||||||
|
return new TimeSpan(0, hours, minutes, seconds, ms)
|
||||||
|
+ TimeSpan.FromMicroseconds(us)
|
||||||
|
+ TimeSpan.FromTicks(ns / 100); // 1 tick = 100 ns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try .NET TimeSpan.Parse (handles "hh:mm:ss")
|
||||||
|
if (TimeSpan.TryParse(s, out var ts)) return ts;
|
||||||
|
|
||||||
|
throw new FormatException($"Cannot parse duration string: \"{s}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatDuration(TimeSpan ts)
|
||||||
|
{
|
||||||
|
if (ts.TotalMilliseconds < 1) return $"{(long)ts.TotalNanoseconds}ns";
|
||||||
|
if (ts.TotalSeconds < 1) return $"{(long)ts.TotalMilliseconds}ms";
|
||||||
|
if (ts.TotalMinutes < 1) return $"{(long)ts.TotalSeconds}s";
|
||||||
|
if (ts.TotalHours < 1) return $"{ts.Minutes}m{ts.Seconds}s";
|
||||||
|
return $"{(int)ts.TotalHours}h{ts.Minutes}m{ts.Seconds}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a TLS version string ("1.2", "1.3", "TLS12") to <see cref="SslProtocols"/>.
|
||||||
|
/// Mirrors Go <c>parseTLSVersion</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TlsVersionJsonConverter : JsonConverter<SslProtocols>
|
||||||
|
{
|
||||||
|
public override SslProtocols Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var raw = reader.GetString()?.Trim() ?? string.Empty;
|
||||||
|
return Parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, SslProtocols value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteStringValue(value.ToString());
|
||||||
|
|
||||||
|
public static SslProtocols Parse(string s) => s.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
"1.2" or "TLS12" or "TLSV1.2" => SslProtocols.Tls12,
|
||||||
|
"1.3" or "TLS13" or "TLSV1.3" => SslProtocols.Tls13,
|
||||||
|
_ => throw new FormatException($"Unknown TLS version: \"{s}\""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates and normalises a NATS URL string (nats://host:port).
|
||||||
|
/// Mirrors Go <c>parseURL</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NatsUrlJsonConverter : JsonConverter<string>
|
||||||
|
{
|
||||||
|
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var raw = reader.GetString() ?? string.Empty;
|
||||||
|
return Normalise(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteStringValue(value);
|
||||||
|
|
||||||
|
public static string Normalise(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url)) return url;
|
||||||
|
url = url.Trim();
|
||||||
|
if (!url.Contains("://")) url = "nats://" + url;
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||||
|
throw new FormatException($"Invalid NATS URL: \"{url}\"");
|
||||||
|
return uri.ToString().TrimEnd('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a storage size string ("1GB", "512MB", "1024") to a byte count (long).
|
||||||
|
/// Mirrors Go <c>getStorageSize</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorageSizeJsonConverter : JsonConverter<long>
|
||||||
|
{
|
||||||
|
private static readonly Regex Pattern = new(@"^(\d+(?:\.\d+)?)\s*([KMGT]?B?)?$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
return reader.GetInt64();
|
||||||
|
}
|
||||||
|
var raw = reader.GetString() ?? "0";
|
||||||
|
return Parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteNumberValue(value);
|
||||||
|
|
||||||
|
public static long Parse(string s)
|
||||||
|
{
|
||||||
|
if (long.TryParse(s, out var n)) return n;
|
||||||
|
var m = Pattern.Match(s.Trim());
|
||||||
|
if (!m.Success) throw new FormatException($"Invalid storage size: \"{s}\"");
|
||||||
|
var num = double.Parse(m.Groups[1].Value);
|
||||||
|
var suffix = m.Groups[2].Value.ToUpperInvariant();
|
||||||
|
return suffix switch
|
||||||
|
{
|
||||||
|
"K" or "KB" => (long)(num * 1024),
|
||||||
|
"M" or "MB" => (long)(num * 1024 * 1024),
|
||||||
|
"G" or "GB" => (long)(num * 1024 * 1024 * 1024),
|
||||||
|
"T" or "TB" => (long)(num * 1024L * 1024 * 1024 * 1024),
|
||||||
|
_ => (long)num,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Adapted from server/opts.go in the NATS server Go source.
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and binds NATS server configuration from an <c>appsettings.json</c> file.
|
||||||
|
/// Replaces the Go <c>processConfigFile</c> / <c>processConfigFileLine</c> pipeline
|
||||||
|
/// and all <c>parse*</c> helper functions in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
public static class ServerOptionsConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="JsonSerializerOptions"/> instance pre-configured with all
|
||||||
|
/// NATS-specific JSON converters.
|
||||||
|
/// </summary>
|
||||||
|
public static JsonSerializerOptions CreateJsonOptions()
|
||||||
|
{
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
};
|
||||||
|
opts.Converters.Add(new NatsDurationJsonConverter());
|
||||||
|
opts.Converters.Add(new TlsVersionJsonConverter());
|
||||||
|
opts.Converters.Add(new StorageSizeJsonConverter());
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a JSON file at <paramref name="path"/> and returns a bound
|
||||||
|
/// <see cref="ServerOptions"/> instance.
|
||||||
|
/// Mirrors Go <c>ProcessConfigFile</c> and <c>Options.ProcessConfigFile</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static ServerOptions ProcessConfigFile(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path))
|
||||||
|
throw new FileNotFoundException($"Configuration file not found: {path}", path);
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path, Encoding.UTF8);
|
||||||
|
return ProcessConfigString(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialises a JSON string and returns a bound <see cref="ServerOptions"/> instance.
|
||||||
|
/// Mirrors Go <c>Options.ProcessConfigString</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static ServerOptions ProcessConfigString(string json)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNullOrEmpty(json);
|
||||||
|
var opts = JsonSerializer.Deserialize<ServerOptions>(json, CreateJsonOptions())
|
||||||
|
?? new ServerOptions();
|
||||||
|
PostProcess(opts);
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds a pre-built <see cref="IConfiguration"/> (e.g. from an ASP.NET Core host)
|
||||||
|
/// to a <see cref="ServerOptions"/> instance.
|
||||||
|
/// The configuration section should be the root or a named section such as "NatsServer".
|
||||||
|
/// </summary>
|
||||||
|
public static void BindConfiguration(IConfiguration config, ServerOptions target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(config);
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
config.Bind(target);
|
||||||
|
PostProcess(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Post-processing
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies defaults and cross-field validation after loading.
|
||||||
|
/// Mirrors the end of <c>Options.processConfigFile</c> and
|
||||||
|
/// <c>configureSystemAccount</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
private static void PostProcess(ServerOptions opts)
|
||||||
|
{
|
||||||
|
// Apply default port if not set.
|
||||||
|
if (opts.Port == 0) opts.Port = ServerConstants.DefaultPort;
|
||||||
|
|
||||||
|
// Apply default host if not set.
|
||||||
|
if (string.IsNullOrEmpty(opts.Host)) opts.Host = ServerConstants.DefaultHost;
|
||||||
|
|
||||||
|
// Apply default max payload.
|
||||||
|
if (opts.MaxPayload == 0) opts.MaxPayload = ServerConstants.MaxPayload;
|
||||||
|
|
||||||
|
// Apply default auth timeout.
|
||||||
|
if (opts.AuthTimeout == 0) opts.AuthTimeout = ServerConstants.DefaultAuthTimeout;
|
||||||
|
|
||||||
|
// Ensure SystemAccount defaults if not set.
|
||||||
|
ConfigureSystemAccount(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up the system account name from options.
|
||||||
|
/// Mirrors Go <c>configureSystemAccount</c> in server/opts.go.
|
||||||
|
/// </summary>
|
||||||
|
private static void ConfigureSystemAccount(ServerOptions opts)
|
||||||
|
{
|
||||||
|
// If system account already set, nothing to do.
|
||||||
|
if (!string.IsNullOrEmpty(opts.SystemAccount)) return;
|
||||||
|
// Default to "$SYS" if not explicitly disabled.
|
||||||
|
opts.SystemAccount = ServerConstants.DefaultSystemAccount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,12 @@ public static class ServerConstants
|
|||||||
// Auth timeout — mirrors AUTH_TIMEOUT.
|
// Auth timeout — mirrors AUTH_TIMEOUT.
|
||||||
public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2);
|
public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
// Default auth timeout as a double (seconds) — used by ServerOptions.AuthTimeout.
|
||||||
|
public const double DefaultAuthTimeout = 2.0;
|
||||||
|
|
||||||
|
// Maximum payload size alias used by config binding — mirrors MAX_PAYLOAD_SIZE.
|
||||||
|
public const int MaxPayload = MaxPayloadSize;
|
||||||
|
|
||||||
// How often pings are sent — mirrors DEFAULT_PING_INTERVAL.
|
// How often pings are sent — mirrors DEFAULT_PING_INTERVAL.
|
||||||
public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2);
|
public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ZB.MOM.NatsNet.Server.Auth;
|
using ZB.MOM.NatsNet.Server.Auth;
|
||||||
|
|
||||||
@@ -31,20 +32,28 @@ public sealed partial class ServerOptions
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public string ConfigFile { get; set; } = string.Empty;
|
public string ConfigFile { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("server_name")]
|
||||||
public string ServerName { get; set; } = string.Empty;
|
public string ServerName { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("host")]
|
||||||
public string Host { get; set; } = string.Empty;
|
public string Host { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("port")]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public bool DontListen { get; set; }
|
public bool DontListen { get; set; }
|
||||||
|
[JsonPropertyName("client_advertise")]
|
||||||
public string ClientAdvertise { get; set; } = string.Empty;
|
public string ClientAdvertise { get; set; } = string.Empty;
|
||||||
public bool CheckConfig { get; set; }
|
public bool CheckConfig { get; set; }
|
||||||
|
[JsonPropertyName("pid_file")]
|
||||||
public string PidFile { get; set; } = string.Empty;
|
public string PidFile { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("ports_file_dir")]
|
||||||
public string PortsFileDir { get; set; } = string.Empty;
|
public string PortsFileDir { get; set; } = string.Empty;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Logging & Debugging
|
// Logging & Debugging
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("trace")]
|
||||||
public bool Trace { get; set; }
|
public bool Trace { get; set; }
|
||||||
|
[JsonPropertyName("debug")]
|
||||||
public bool Debug { get; set; }
|
public bool Debug { get; set; }
|
||||||
public bool TraceVerbose { get; set; }
|
public bool TraceVerbose { get; set; }
|
||||||
public bool TraceHeaders { get; set; }
|
public bool TraceHeaders { get; set; }
|
||||||
@@ -52,7 +61,9 @@ public sealed partial class ServerOptions
|
|||||||
public bool NoSigs { get; set; }
|
public bool NoSigs { get; set; }
|
||||||
public bool Logtime { get; set; }
|
public bool Logtime { get; set; }
|
||||||
public bool LogtimeUtc { get; set; }
|
public bool LogtimeUtc { get; set; }
|
||||||
|
[JsonPropertyName("logfile")]
|
||||||
public string LogFile { get; set; } = string.Empty;
|
public string LogFile { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("log_size_limit")]
|
||||||
public long LogSizeLimit { get; set; }
|
public long LogSizeLimit { get; set; }
|
||||||
public long LogMaxFiles { get; set; }
|
public long LogMaxFiles { get; set; }
|
||||||
public bool Syslog { get; set; }
|
public bool Syslog { get; set; }
|
||||||
@@ -65,11 +76,14 @@ public sealed partial class ServerOptions
|
|||||||
// Networking & Limits
|
// Networking & Limits
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("max_connections")]
|
||||||
public int MaxConn { get; set; }
|
public int MaxConn { get; set; }
|
||||||
public int MaxSubs { get; set; }
|
public int MaxSubs { get; set; }
|
||||||
public byte MaxSubTokens { get; set; }
|
public byte MaxSubTokens { get; set; }
|
||||||
public int MaxControlLine { get; set; }
|
public int MaxControlLine { get; set; }
|
||||||
|
[JsonPropertyName("max_payload")]
|
||||||
public int MaxPayload { get; set; }
|
public int MaxPayload { get; set; }
|
||||||
|
[JsonPropertyName("max_pending")]
|
||||||
public long MaxPending { get; set; }
|
public long MaxPending { get; set; }
|
||||||
public bool NoFastProducerStall { get; set; }
|
public bool NoFastProducerStall { get; set; }
|
||||||
public bool ProxyRequired { get; set; }
|
public bool ProxyRequired { get; set; }
|
||||||
@@ -80,11 +94,16 @@ public sealed partial class ServerOptions
|
|||||||
// Connectivity
|
// Connectivity
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("ping_interval")]
|
||||||
public TimeSpan PingInterval { get; set; }
|
public TimeSpan PingInterval { get; set; }
|
||||||
|
[JsonPropertyName("ping_max")]
|
||||||
public int MaxPingsOut { get; set; }
|
public int MaxPingsOut { get; set; }
|
||||||
|
[JsonPropertyName("write_deadline")]
|
||||||
public TimeSpan WriteDeadline { get; set; }
|
public TimeSpan WriteDeadline { get; set; }
|
||||||
public WriteTimeoutPolicy WriteTimeout { get; set; }
|
public WriteTimeoutPolicy WriteTimeout { get; set; }
|
||||||
|
[JsonPropertyName("lame_duck_duration")]
|
||||||
public TimeSpan LameDuckDuration { get; set; }
|
public TimeSpan LameDuckDuration { get; set; }
|
||||||
|
[JsonPropertyName("lame_duck_grace_period")]
|
||||||
public TimeSpan LameDuckGracePeriod { get; set; }
|
public TimeSpan LameDuckGracePeriod { get; set; }
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -92,23 +111,33 @@ public sealed partial class ServerOptions
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public string HttpHost { get; set; } = string.Empty;
|
public string HttpHost { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("http_port")]
|
||||||
public int HttpPort { get; set; }
|
public int HttpPort { get; set; }
|
||||||
|
[JsonPropertyName("http_base_path")]
|
||||||
public string HttpBasePath { get; set; } = string.Empty;
|
public string HttpBasePath { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("https_port")]
|
||||||
public int HttpsPort { get; set; }
|
public int HttpsPort { get; set; }
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Authentication & Authorization
|
// Authentication & Authorization
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("username")]
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("password")]
|
||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("authorization")]
|
||||||
public string Authorization { get; set; } = string.Empty;
|
public string Authorization { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("auth_timeout")]
|
||||||
public double AuthTimeout { get; set; }
|
public double AuthTimeout { get; set; }
|
||||||
|
[JsonPropertyName("no_auth_user")]
|
||||||
public string NoAuthUser { get; set; } = string.Empty;
|
public string NoAuthUser { get; set; } = string.Empty;
|
||||||
public string DefaultSentinel { get; set; } = string.Empty;
|
public string DefaultSentinel { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("system_account")]
|
||||||
public string SystemAccount { get; set; } = string.Empty;
|
public string SystemAccount { get; set; } = string.Empty;
|
||||||
public bool NoSystemAccount { get; set; }
|
public bool NoSystemAccount { get; set; }
|
||||||
/// <summary>Parsed account objects from config. Mirrors Go opts.Accounts.</summary>
|
/// <summary>Parsed account objects from config. Mirrors Go opts.Accounts.</summary>
|
||||||
|
[JsonPropertyName("accounts")]
|
||||||
public List<Account> Accounts { get; set; } = [];
|
public List<Account> Accounts { get; set; } = [];
|
||||||
public AuthCalloutOpts? AuthCallout { get; set; }
|
public AuthCalloutOpts? AuthCallout { get; set; }
|
||||||
public bool AlwaysEnableNonce { get; set; }
|
public bool AlwaysEnableNonce { get; set; }
|
||||||
@@ -148,8 +177,11 @@ public sealed partial class ServerOptions
|
|||||||
// Cluster / Gateway / Leaf / WebSocket / MQTT
|
// Cluster / Gateway / Leaf / WebSocket / MQTT
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("cluster")]
|
||||||
public ClusterOpts Cluster { get; set; } = new();
|
public ClusterOpts Cluster { get; set; } = new();
|
||||||
|
[JsonPropertyName("gateway")]
|
||||||
public GatewayOpts Gateway { get; set; } = new();
|
public GatewayOpts Gateway { get; set; } = new();
|
||||||
|
[JsonPropertyName("leafnodes")]
|
||||||
public LeafNodeOpts LeafNode { get; set; } = new();
|
public LeafNodeOpts LeafNode { get; set; } = new();
|
||||||
public WebsocketOpts Websocket { get; set; } = new();
|
public WebsocketOpts Websocket { get; set; } = new();
|
||||||
public MqttOpts Mqtt { get; set; } = new();
|
public MqttOpts Mqtt { get; set; } = new();
|
||||||
@@ -165,6 +197,7 @@ public sealed partial class ServerOptions
|
|||||||
// JetStream
|
// JetStream
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[JsonPropertyName("jetstream")]
|
||||||
public bool JetStream { get; set; }
|
public bool JetStream { get; set; }
|
||||||
public bool NoJetStreamStrict { get; set; }
|
public bool NoJetStreamStrict { get; set; }
|
||||||
public long JetStreamMaxMemory { get; set; }
|
public long JetStreamMaxMemory { get; set; }
|
||||||
@@ -184,6 +217,7 @@ public sealed partial class ServerOptions
|
|||||||
public bool JetStreamMetaCompactSync { get; set; }
|
public bool JetStreamMetaCompactSync { get; set; }
|
||||||
public int StreamMaxBufferedMsgs { get; set; }
|
public int StreamMaxBufferedMsgs { get; set; }
|
||||||
public long StreamMaxBufferedSize { get; set; }
|
public long StreamMaxBufferedSize { get; set; }
|
||||||
|
[JsonPropertyName("store_dir")]
|
||||||
public string StoreDir { get; set; } = string.Empty;
|
public string StoreDir { get; set; } = string.Empty;
|
||||||
public TimeSpan SyncInterval { get; set; }
|
public TimeSpan SyncInterval { get; set; }
|
||||||
public bool SyncAlways { get; set; }
|
public bool SyncAlways { get; set; }
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="*" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="*" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="*" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="*" />
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="*" />
|
<PackageReference Include="BCrypt.Net-Next" Version="*" />
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2012-2025 The NATS Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Tests.Config;
|
||||||
|
|
||||||
|
using ZB.MOM.NatsNet.Server.Config;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class ServerOptionsConfigurationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProcessConfigString_MinimalJson_SetsPort()
|
||||||
|
{
|
||||||
|
var opts = ServerOptionsConfiguration.ProcessConfigString("""{"port": 4222}""");
|
||||||
|
opts.Port.ShouldBe(4222);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessConfigString_EmptyJson_AppliesDefaults()
|
||||||
|
{
|
||||||
|
var opts = ServerOptionsConfiguration.ProcessConfigString("{}");
|
||||||
|
opts.Port.ShouldBe(ServerConstants.DefaultPort);
|
||||||
|
opts.Host.ShouldBe(ServerConstants.DefaultHost);
|
||||||
|
opts.MaxPayload.ShouldBe(ServerConstants.MaxPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessConfigString_AllBasicFields_Roundtrip()
|
||||||
|
{
|
||||||
|
var json = """
|
||||||
|
{
|
||||||
|
"port": 5222,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"server_name": "test-server",
|
||||||
|
"debug": true,
|
||||||
|
"trace": true,
|
||||||
|
"max_connections": 100,
|
||||||
|
"auth_timeout": 2.0
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var opts = ServerOptionsConfiguration.ProcessConfigString(json);
|
||||||
|
opts.Port.ShouldBe(5222);
|
||||||
|
opts.Host.ShouldBe("127.0.0.1");
|
||||||
|
opts.ServerName.ShouldBe("test-server");
|
||||||
|
opts.Debug.ShouldBeTrue();
|
||||||
|
opts.Trace.ShouldBeTrue();
|
||||||
|
opts.MaxConn.ShouldBe(100);
|
||||||
|
opts.AuthTimeout.ShouldBe(2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessConfigFile_FileNotFound_Throws()
|
||||||
|
{
|
||||||
|
Should.Throw<FileNotFoundException>(() =>
|
||||||
|
ServerOptionsConfiguration.ProcessConfigFile("/nonexistent/path.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessConfigFile_ValidFile_ReturnsOptions()
|
||||||
|
{
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
File.WriteAllText(tmpFile, """{"port": 9090, "server_name": "from-file"}""");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var opts = ServerOptionsConfiguration.ProcessConfigFile(tmpFile);
|
||||||
|
opts.Port.ShouldBe(9090);
|
||||||
|
opts.ServerName.ShouldBe("from-file");
|
||||||
|
}
|
||||||
|
finally { File.Delete(tmpFile); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NatsDurationJsonConverterTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("2s", 0, 0, 2, 0)]
|
||||||
|
[InlineData("100ms", 0, 0, 0, 100)]
|
||||||
|
[InlineData("1h30m", 1, 30, 0, 0)]
|
||||||
|
public void Parse_ValidDurationStrings_ReturnsCorrectTimeSpan(
|
||||||
|
string input, int hours, int minutes, int seconds, int ms)
|
||||||
|
{
|
||||||
|
var expected = new TimeSpan(0, hours, minutes, seconds, ms);
|
||||||
|
NatsDurationJsonConverter.Parse(input).ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_FiveMinutesTenSeconds_ReturnsCorrectSpan()
|
||||||
|
{
|
||||||
|
var result = NatsDurationJsonConverter.Parse("5m10s");
|
||||||
|
result.ShouldBe(TimeSpan.FromSeconds(310));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Parse_InvalidString_ThrowsFormatException()
|
||||||
|
{
|
||||||
|
Should.Throw<FormatException>(() => NatsDurationJsonConverter.Parse("notaduration"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StorageSizeJsonConverterTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("1GB", 1L * 1024 * 1024 * 1024)]
|
||||||
|
[InlineData("512MB", 512L * 1024 * 1024)]
|
||||||
|
[InlineData("1KB", 1024L)]
|
||||||
|
[InlineData("1024", 1024L)]
|
||||||
|
public void Parse_ValidSizeStrings_ReturnsBytes(string input, long expectedBytes)
|
||||||
|
{
|
||||||
|
StorageSizeJsonConverter.Parse(input).ShouldBe(expectedBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-26 21:59:34 UTC
|
Generated: 2026-02-26 22:18:29 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
@@ -13,9 +13,9 @@ Generated: 2026-02-26 21:59:34 UTC
|
|||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| complete | 3503 |
|
| complete | 3570 |
|
||||||
| n_a | 77 |
|
| n_a | 77 |
|
||||||
| stub | 93 |
|
| stub | 26 |
|
||||||
|
|
||||||
## Unit Tests (3257 total)
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
@@ -35,4 +35,4 @@ Generated: 2026-02-26 21:59:34 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**4091/6942 items complete (58.9%)**
|
**4158/6942 items complete (59.9%)**
|
||||||
|
|||||||
38
reports/report_8253f97.md
Normal file
38
reports/report_8253f97.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
|
Generated: 2026-02-26 22:18:29 UTC
|
||||||
|
|
||||||
|
## Modules (12 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 11 |
|
||||||
|
| not_started | 1 |
|
||||||
|
|
||||||
|
## Features (3673 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 3570 |
|
||||||
|
| n_a | 77 |
|
||||||
|
| stub | 26 |
|
||||||
|
|
||||||
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 319 |
|
||||||
|
| n_a | 181 |
|
||||||
|
| not_started | 2533 |
|
||||||
|
| stub | 224 |
|
||||||
|
|
||||||
|
## Library Mappings (36 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| mapped | 36 |
|
||||||
|
|
||||||
|
|
||||||
|
## Overall Progress
|
||||||
|
|
||||||
|
**4158/6942 items complete (59.9%)**
|
||||||
Reference in New Issue
Block a user