feat(lmxproxy): phase 1 — v2 protocol types and domain model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-21 23:41:56 -04:00
parent 08d2a07d8b
commit 0d63fb1105
87 changed files with 3389 additions and 956 deletions

View File

@@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
namespace ZB.MOM.WW.LmxProxy.Host.Configuration
{
/// <summary>
/// Validates LmxProxy configuration settings on startup.
/// </summary>
public static class ConfigurationValidator
{
private static readonly ILogger Logger = Log.ForContext(typeof(ConfigurationValidator));
/// <summary>
/// Validates the provided configuration and returns a list of validation errors.
/// </summary>
/// <param name="configuration">The configuration to validate.</param>
/// <returns>A list of validation error messages. Empty if configuration is valid.</returns>
public static List<string> Validate(LmxProxyConfiguration configuration)
{
var errors = new List<string>();
if (configuration == null)
{
errors.Add("Configuration is null");
return errors;
}
// Validate gRPC port
if (configuration.GrpcPort <= 0 || configuration.GrpcPort > 65535)
{
errors.Add($"Invalid gRPC port: {configuration.GrpcPort}. Must be between 1 and 65535.");
}
// Validate API key configuration file
if (string.IsNullOrWhiteSpace(configuration.ApiKeyConfigFile))
{
errors.Add("API key configuration file path is not specified.");
}
// Validate Connection settings
if (configuration.Connection != null)
{
ValidateConnectionConfiguration(configuration.Connection, errors);
}
else
{
errors.Add("Connection configuration is missing.");
}
// Validate Subscription settings
if (configuration.Subscription != null)
{
ValidateSubscriptionConfiguration(configuration.Subscription, errors);
}
// Validate Service Recovery settings
if (configuration.ServiceRecovery != null)
{
ValidateServiceRecoveryConfiguration(configuration.ServiceRecovery, errors);
}
// Validate TLS settings
if (configuration.Tls != null)
{
if (!configuration.Tls.Validate())
{
errors.Add("TLS configuration validation failed. Check the logs for details.");
}
}
return errors;
}
private static void ValidateConnectionConfiguration(ConnectionConfiguration config, List<string> errors)
{
if (config.MonitorIntervalSeconds <= 0)
{
errors.Add(
$"Invalid monitor interval: {config.MonitorIntervalSeconds} seconds. Must be greater than 0.");
}
if (config.ConnectionTimeoutSeconds <= 0)
{
errors.Add(
$"Invalid connection timeout: {config.ConnectionTimeoutSeconds} seconds. Must be greater than 0.");
}
if (config.ReadTimeoutSeconds <= 0)
{
errors.Add($"Invalid read timeout: {config.ReadTimeoutSeconds} seconds. Must be greater than 0.");
}
if (config.WriteTimeoutSeconds <= 0)
{
errors.Add($"Invalid write timeout: {config.WriteTimeoutSeconds} seconds. Must be greater than 0.");
}
if (config.MaxConcurrentOperations.HasValue && config.MaxConcurrentOperations.Value <= 0)
{
errors.Add(
$"Invalid max concurrent operations: {config.MaxConcurrentOperations}. Must be greater than 0.");
}
// Validate node and galaxy names if provided
if (!string.IsNullOrWhiteSpace(config.NodeName) && config.NodeName?.Length > 255)
{
errors.Add($"Node name is too long: {config.NodeName.Length} characters. Maximum is 255.");
}
if (!string.IsNullOrWhiteSpace(config.GalaxyName) && config.GalaxyName?.Length > 255)
{
errors.Add($"Galaxy name is too long: {config.GalaxyName.Length} characters. Maximum is 255.");
}
}
private static void ValidateSubscriptionConfiguration(SubscriptionConfiguration config, List<string> errors)
{
if (config.ChannelCapacity <= 0)
{
errors.Add($"Invalid channel capacity: {config.ChannelCapacity}. Must be greater than 0.");
}
if (config.ChannelCapacity > 100000)
{
errors.Add($"Channel capacity too large: {config.ChannelCapacity}. Maximum recommended is 100000.");
}
string[] validChannelModes = { "DropOldest", "DropNewest", "Wait" };
if (!validChannelModes.Contains(config.ChannelFullMode))
{
errors.Add(
$"Invalid channel full mode: {config.ChannelFullMode}. Valid values are: {string.Join(", ", validChannelModes)}");
}
}
private static void ValidateServiceRecoveryConfiguration(ServiceRecoveryConfiguration config,
List<string> errors)
{
if (config.FirstFailureDelayMinutes < 0)
{
errors.Add(
$"Invalid first failure delay: {config.FirstFailureDelayMinutes} minutes. Must be 0 or greater.");
}
if (config.SecondFailureDelayMinutes < 0)
{
errors.Add(
$"Invalid second failure delay: {config.SecondFailureDelayMinutes} minutes. Must be 0 or greater.");
}
if (config.SubsequentFailureDelayMinutes < 0)
{
errors.Add(
$"Invalid subsequent failure delay: {config.SubsequentFailureDelayMinutes} minutes. Must be 0 or greater.");
}
if (config.ResetPeriodDays <= 0)
{
errors.Add($"Invalid reset period: {config.ResetPeriodDays} days. Must be greater than 0.");
}
}
/// <summary>
/// Logs validation results and returns whether the configuration is valid.
/// </summary>
/// <param name="configuration">The configuration to validate.</param>
/// <returns>True if configuration is valid; otherwise, false.</returns>
public static bool ValidateAndLog(LmxProxyConfiguration configuration)
{
List<string> errors = Validate(configuration);
if (errors.Any())
{
Logger.Error("Configuration validation failed with {ErrorCount} errors:", errors.Count);
foreach (string? error in errors)
{
Logger.Error(" - {ValidationError}", error);
}
return false;
}
Logger.Information("Configuration validation successful");
return true;
}
/// <summary>
/// Throws an exception if the configuration is invalid.
/// </summary>
/// <param name="configuration">The configuration to validate.</param>
/// <exception cref="InvalidOperationException">Thrown when configuration is invalid.</exception>
public static void ValidateOrThrow(LmxProxyConfiguration configuration)
{
List<string> errors = Validate(configuration);
if (errors.Any())
{
string message = $"Configuration validation failed with {errors.Count} error(s):\n" +
string.Join("\n", errors.Select(e => $" - {e}"));
throw new InvalidOperationException(message);
}
}
}
}