using System.Text.Json; using System.Text.Json.Serialization; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers; /// /// Mutable, all-nullable form model for the driver resilience override. Binds the typed /// fields in DriverResilienceSection; null/blank = "use the driver's tier default", so a /// blank form serializes back to null (preserving DriverInstance.ResilienceConfig = null). /// Emits / reads the exact override JSON shape DriverResilienceOptionsParser consumes. /// public sealed class ResilienceFormModel { public static readonly string[] Capabilities = ["Read", "Write", "Discover", "Subscribe", "Probe", "AlarmSubscribe", "AlarmAcknowledge", "HistoryRead"]; public int? BulkheadMaxConcurrent { get; set; } public int? BulkheadMaxQueue { get; set; } public int? RecycleIntervalSeconds { get; set; } // capability name -> (timeout, retry, breaker), each nullable. public Dictionary Policies { get; set; } = Capabilities.ToDictionary(c => c, _ => new CapabilityRow(), StringComparer.OrdinalIgnoreCase); public sealed class CapabilityRow { public int? TimeoutSeconds { get; set; } public int? RetryCount { get; set; } public int? BreakerFailureThreshold { get; set; } public bool IsEmpty => TimeoutSeconds is null && RetryCount is null && BreakerFailureThreshold is null; } private static readonly JsonSerializerOptions ReadOpts = new() { PropertyNameCaseInsensitive = true }; private static readonly JsonSerializerOptions WriteOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; public static ResilienceFormModel FromJson(string? json) { var model = new ResilienceFormModel(); if (string.IsNullOrWhiteSpace(json)) return model; Shape? shape; try { shape = JsonSerializer.Deserialize(json, ReadOpts); } catch (JsonException) { return model; } // malformed -> empty form; raw view (next task) shows the text if (shape is null) return model; model.BulkheadMaxConcurrent = shape.BulkheadMaxConcurrent; model.BulkheadMaxQueue = shape.BulkheadMaxQueue; model.RecycleIntervalSeconds = shape.RecycleIntervalSeconds; if (shape.CapabilityPolicies is not null) foreach (var (cap, p) in shape.CapabilityPolicies) if (model.Policies.TryGetValue(cap, out var row)) { row.TimeoutSeconds = p.TimeoutSeconds; row.RetryCount = p.RetryCount; row.BreakerFailureThreshold = p.BreakerFailureThreshold; } return model; } /// Emit only the non-null overrides; returns null when nothing is overridden. public string? ToJson() { var caps = Policies .Where(kv => !kv.Value.IsEmpty) .ToDictionary(kv => kv.Key, kv => new PolicyShape { TimeoutSeconds = kv.Value.TimeoutSeconds, RetryCount = kv.Value.RetryCount, BreakerFailureThreshold = kv.Value.BreakerFailureThreshold, }); var hasAny = BulkheadMaxConcurrent is not null || BulkheadMaxQueue is not null || RecycleIntervalSeconds is not null || caps.Count > 0; if (!hasAny) return null; var shape = new Shape { BulkheadMaxConcurrent = BulkheadMaxConcurrent, BulkheadMaxQueue = BulkheadMaxQueue, RecycleIntervalSeconds = RecycleIntervalSeconds, CapabilityPolicies = caps.Count > 0 ? caps : null, }; return JsonSerializer.Serialize(shape, WriteOpts); } private sealed class Shape { public int? BulkheadMaxConcurrent { get; set; } public int? BulkheadMaxQueue { get; set; } public int? RecycleIntervalSeconds { get; set; } public Dictionary? CapabilityPolicies { get; set; } } private sealed class PolicyShape { public int? TimeoutSeconds { get; set; } public int? RetryCount { get; set; } public int? BreakerFailureThreshold { get; set; } } }