using Microsoft.Extensions.Options; namespace Mbproxy.Options; public sealed class MbproxyOptions { public BcdTagListOptions BcdTags { get; init; } = new(); public IReadOnlyList Plcs { get; init; } = []; public int AdminPort { get; init; } = 8080; public ConnectionOptions Connection { get; init; } = new(); public ResilienceOptions Resilience { get; init; } = new(); /// /// Phase 11 — service-wide response-cache settings. The cache is opt-in /// per-tag (); this section configures the /// safety knobs that gate / bound the cache. /// public CacheOptions Cache { get; init; } = new(); } /// /// Phase 11 — service-wide response-cache knobs. The cache is OFF by default for every /// tag; this section governs the limits when an operator opts a tag in. /// public sealed class CacheOptions { /// /// Gate for any greater than 60_000 ms. /// Defaults to false so accidentally-stale-for-an-hour deployments are caught /// at reload validation. Set to true to explicitly allow long TTLs. /// public bool AllowLongTtl { get; init; } = false; /// /// LRU cap on the number of entries per-PLC. Past this cap, the next insert evicts /// the least-recently-used entry. Defaults to 1000 — comfortable for a 54-PLC fleet /// with short TTLs. /// public int MaxEntriesPerPlc { get; init; } = 1000; /// /// Background eviction loop tick in milliseconds. Each tick scans the cache and /// removes entries past their ExpiresAtUtc. Defaults to 5000 ms; values below /// 100 ms are clamped at 100 to avoid pathologically tight loops. /// public int EvictionIntervalMs { get; init; } = 5000; } /// /// Schema-level validation for . /// Business-rule validation (duplicate addresses, port conflicts) is deferred to phase 06. /// public sealed class MbproxyOptionsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, MbproxyOptions options) { var errors = new List(); bool allowLongTtl = options.Cache.AllowLongTtl; foreach (var tag in options.BcdTags.Global) { if (tag.Width != 16 && tag.Width != 32) errors.Add($"BcdTags.Global: Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32."); ValidateCacheTtl(errors, $"BcdTags.Global Address {tag.Address}", tag.CacheTtlMs, allowLongTtl); } for (int i = 0; i < options.Plcs.Count; i++) { var plc = options.Plcs[i]; // Phase 11 — per-PLC default TTL bounds. if (plc.DefaultCacheTtlMs < 0) errors.Add($"Plcs[{i}] ({plc.Name}): DefaultCacheTtlMs must be >= 0."); else if (plc.DefaultCacheTtlMs > 60_000 && !allowLongTtl) errors.Add( $"Plcs[{i}] ({plc.Name}): DefaultCacheTtlMs={plc.DefaultCacheTtlMs} exceeds the 60_000 ms safety cap; " + $"set Cache.AllowLongTtl=true to opt in."); if (plc.BcdTags is { } overrides) { foreach (var tag in overrides.Add) { if (tag.Width != 16 && tag.Width != 32) errors.Add($"Plcs[{i}] ({plc.Name}): BcdTags.Add Address {tag.Address} has invalid Width {tag.Width}; must be 16 or 32."); ValidateCacheTtl(errors, $"Plcs[{i}] ({plc.Name}) BcdTags.Add Address {tag.Address}", tag.CacheTtlMs, allowLongTtl); } } } // Cache section ranges. if (options.Cache.MaxEntriesPerPlc < 0) errors.Add($"Cache.MaxEntriesPerPlc must be >= 0; got {options.Cache.MaxEntriesPerPlc}."); if (options.Cache.EvictionIntervalMs < 0) errors.Add($"Cache.EvictionIntervalMs must be >= 0; got {options.Cache.EvictionIntervalMs}."); return errors.Count > 0 ? ValidateOptionsResult.Fail(errors) : ValidateOptionsResult.Success; } private static void ValidateCacheTtl(List errors, string context, int? ttlMs, bool allowLongTtl) { if (ttlMs is null) return; int value = ttlMs.Value; if (value < 0) errors.Add($"{context}: CacheTtlMs must be >= 0; got {value}."); else if (value > 60_000 && !allowLongTtl) errors.Add( $"{context}: CacheTtlMs={value} exceeds the 60_000 ms safety cap; " + $"set Cache.AllowLongTtl=true to opt in."); } }