feat(adminui): typed resilience override form model + tests
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string, CapabilityRow> Policies { get; set; } =
|
||||
Capabilities.ToDictionary(c => c, _ => new CapabilityRow());
|
||||
|
||||
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<Shape>(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;
|
||||
}
|
||||
|
||||
/// <summary>Emit only the non-null overrides; returns null when nothing is overridden.</summary>
|
||||
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<string, PolicyShape>? CapabilityPolicies { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PolicyShape
|
||||
{
|
||||
public int? TimeoutSeconds { get; set; }
|
||||
public int? RetryCount { get; set; }
|
||||
public int? BreakerFailureThreshold { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
|
||||
public class ResilienceFormModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Blank_form_serializes_to_null()
|
||||
=> new ResilienceFormModel().ToJson().ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void Partial_override_round_trips()
|
||||
{
|
||||
var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
|
||||
m.Policies["Read"].TimeoutSeconds = 5;
|
||||
m.Policies["Read"].RetryCount = 5;
|
||||
|
||||
var json = m.ToJson();
|
||||
json.ShouldNotBeNull();
|
||||
|
||||
var back = ResilienceFormModel.FromJson(json);
|
||||
back.BulkheadMaxConcurrent.ShouldBe(16);
|
||||
back.Policies["Read"].TimeoutSeconds.ShouldBe(5);
|
||||
back.Policies["Write"].IsEmpty.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Emitted_json_is_consumable_by_the_runtime_parser()
|
||||
{
|
||||
var m = new ResilienceFormModel { BulkheadMaxConcurrent = 16 };
|
||||
m.Policies["Read"].TimeoutSeconds = 7;
|
||||
|
||||
var opts = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, m.ToJson(), out var diag);
|
||||
diag.ShouldBeNull();
|
||||
opts.BulkheadMaxConcurrent.ShouldBe(16);
|
||||
opts.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(7);
|
||||
opts.Resolve(DriverCapability.Write).RetryCount.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Server\ZB.MOM.WW.OtOpcUa.AdminUI\ZB.MOM.WW.OtOpcUa.AdminUI.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user