using Microsoft.Extensions.Options; using MxGateway.Contracts; namespace MxGateway.Server.Configuration; public sealed class GatewayOptionsValidator : IValidateOptions { private const int MinimumMaxMessageBytes = 1024; private const int MaximumMaxMessageBytes = 256 * 1024 * 1024; public ValidateOptionsResult Validate(string? name, GatewayOptions options) { List failures = []; ValidateAuthentication(options.Authentication, failures); ValidateWorker(options.Worker, failures); ValidateSessions(options.Sessions, failures); ValidateEvents(options.Events, failures); ValidateDashboard(options.Dashboard, failures); ValidateProtocol(options.Protocol, failures); return failures.Count == 0 ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(failures); } private static void ValidateAuthentication(AuthenticationOptions options, List failures) { if (!Enum.IsDefined(options.Mode)) { failures.Add("MxGateway:Authentication:Mode must be a supported authentication mode."); return; } if (options.Mode == AuthenticationMode.ApiKey) { AddIfBlank( options.SqlitePath, "MxGateway:Authentication:SqlitePath is required when API-key authentication is enabled.", failures); AddIfInvalidPath( options.SqlitePath, "MxGateway:Authentication:SqlitePath must be a valid filesystem path.", failures); AddIfBlank( options.PepperSecretName, "MxGateway:Authentication:PepperSecretName is required when API-key authentication is enabled.", failures); } } private static void ValidateWorker(WorkerOptions options, List failures) { AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", failures); AddIfInvalidPath( options.ExecutablePath, "MxGateway:Worker:ExecutablePath must be a valid filesystem path.", failures); if (!string.IsNullOrWhiteSpace(options.ExecutablePath) && !string.Equals(Path.GetExtension(options.ExecutablePath), ".exe", StringComparison.OrdinalIgnoreCase)) { failures.Add("MxGateway:Worker:ExecutablePath must point to a .exe file."); } if (!string.IsNullOrWhiteSpace(options.WorkingDirectory)) { AddIfInvalidPath( options.WorkingDirectory, "MxGateway:Worker:WorkingDirectory must be a valid filesystem path.", failures); } if (!Enum.IsDefined(options.RequiredArchitecture)) { failures.Add("MxGateway:Worker:RequiredArchitecture must be a supported worker architecture."); } AddIfNotPositive( options.StartupTimeoutSeconds, "MxGateway:Worker:StartupTimeoutSeconds must be greater than zero.", failures); AddIfNotPositive( options.ShutdownTimeoutSeconds, "MxGateway:Worker:ShutdownTimeoutSeconds must be greater than zero.", failures); AddIfNotPositive( options.HeartbeatIntervalSeconds, "MxGateway:Worker:HeartbeatIntervalSeconds must be greater than zero.", failures); AddIfNotPositive( options.HeartbeatGraceSeconds, "MxGateway:Worker:HeartbeatGraceSeconds must be greater than zero.", failures); if (options.HeartbeatGraceSeconds < options.HeartbeatIntervalSeconds) { failures.Add( "MxGateway:Worker:HeartbeatGraceSeconds must be greater than or equal to HeartbeatIntervalSeconds."); } if (options.MaxMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes) { failures.Add( $"MxGateway:Worker:MaxMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}."); } } private static void ValidateSessions(SessionOptions options, List failures) { AddIfNotPositive( options.DefaultCommandTimeoutSeconds, "MxGateway:Sessions:DefaultCommandTimeoutSeconds must be greater than zero.", failures); AddIfNotPositive(options.MaxSessions, "MxGateway:Sessions:MaxSessions must be greater than zero.", failures); } private static void ValidateEvents(EventOptions options, List failures) { AddIfNotPositive(options.QueueCapacity, "MxGateway:Events:QueueCapacity must be greater than zero.", failures); if (!Enum.IsDefined(options.BackpressurePolicy)) { failures.Add("MxGateway:Events:BackpressurePolicy must be a supported backpressure policy."); } } private static void ValidateDashboard(DashboardOptions options, List failures) { if (options.Enabled) { AddIfBlank(options.PathBase, "MxGateway:Dashboard:PathBase is required when the dashboard is enabled.", failures); if (!string.IsNullOrWhiteSpace(options.PathBase) && !options.PathBase.StartsWith('/')) { failures.Add("MxGateway:Dashboard:PathBase must start with '/'."); } } AddIfNotPositive( options.SnapshotIntervalMilliseconds, "MxGateway:Dashboard:SnapshotIntervalMilliseconds must be greater than zero.", failures); AddIfNegative( options.RecentFaultLimit, "MxGateway:Dashboard:RecentFaultLimit must be greater than or equal to zero.", failures); AddIfNegative( options.RecentSessionLimit, "MxGateway:Dashboard:RecentSessionLimit must be greater than or equal to zero.", failures); } private static void ValidateProtocol(ProtocolOptions options, List failures) { if (options.WorkerProtocolVersion != GatewayContractInfo.WorkerProtocolVersion) { failures.Add( $"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}."); } } private static void AddIfBlank(string? value, string message, List failures) { if (string.IsNullOrWhiteSpace(value)) { failures.Add(message); } } private static void AddIfNotPositive(int value, string message, List failures) { if (value <= 0) { failures.Add(message); } } private static void AddIfNegative(int value, string message, List failures) { if (value < 0) { failures.Add(message); } } private static void AddIfInvalidPath(string? value, string message, List failures) { if (string.IsNullOrWhiteSpace(value)) { return; } try { _ = Path.GetFullPath(value); } catch (ArgumentException) { failures.Add(message); } catch (NotSupportedException) { failures.Add(message); } catch (PathTooLongException) { failures.Add(message); } } }