Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Plan.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

113 lines
5.3 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
/// <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied
/// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
/// captured by stable identity: added items are new, removed items have to be torn down,
/// changed items have the same identity but at least one field differs.
///
/// OpcUaPublishActor's <c>RebuildAddressSpace</c> consumes this against a real
/// <see cref="Commons.OpcUa.IOpcUaAddressSpaceSink"/> binding so re-applies only mutate the
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
/// drastic schema flips.
/// </summary>
public sealed record Phase7Plan(
IReadOnlyList<EquipmentNode> AddedEquipment,
IReadOnlyList<EquipmentNode> RemovedEquipment,
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<DriverInstancePlan> AddedDrivers,
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms,
IReadOnlyList<GalaxyTagPlan> AddedGalaxyTags,
IReadOnlyList<GalaxyTagPlan> RemovedGalaxyTags,
IReadOnlyList<Phase7Plan.GalaxyTagDelta> ChangedGalaxyTags)
{
/// <summary>Gets a value indicating whether the composition plan contains no changes.</summary>
public bool IsEmpty =>
AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0;
public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
}
public static class Phase7Planner
{
/// <summary>
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
/// Identity is the entity's stable id (EquipmentId, DriverInstanceId, ScriptedAlarmId).
/// Element equality on the projection records doubles as the "did this change" check,
/// so any field difference moves an item from "stable" to ChangedX.
/// </summary>
/// <param name="previous">The previous composition result.</param>
/// <param name="next">The new composition result.</param>
public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next)
{
ArgumentNullException.ThrowIfNull(previous);
ArgumentNullException.ThrowIfNull(next);
var (addedEq, removedEq, changedEq) = DiffById(
previous.EquipmentNodes, next.EquipmentNodes,
n => n.EquipmentId,
(a, b) => new Phase7Plan.EquipmentDelta(a, b));
var (addedDrv, removedDrv, changedDrv) = DiffById(
previous.DriverInstancePlans, next.DriverInstancePlans,
d => d.DriverInstanceId,
(a, b) => new Phase7Plan.DriverDelta(a, b));
var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
a => a.ScriptedAlarmId,
(a, b) => new Phase7Plan.AlarmDelta(a, b));
var (addedGalaxy, removedGalaxy, changedGalaxy) = DiffById(
previous.GalaxyTags, next.GalaxyTags,
t => t.TagId,
(a, b) => new Phase7Plan.GalaxyTagDelta(a, b));
return new Phase7Plan(
addedEq, removedEq, changedEq,
addedDrv, removedDrv, changedDrv,
addedAlarm, removedAlarm, changedAlarm,
addedGalaxy, removedGalaxy, changedGalaxy);
}
private static (IReadOnlyList<T> Added, IReadOnlyList<T> Removed, IReadOnlyList<TDelta> Changed)
DiffById<T, TDelta>(
IReadOnlyList<T> previous,
IReadOnlyList<T> next,
Func<T, string> identity,
Func<T, T, TDelta> deltaFactory) where T : class
{
var prevById = previous.ToDictionary(identity, StringComparer.Ordinal);
var nextById = next.ToDictionary(identity, StringComparer.Ordinal);
var added = new List<T>();
var removed = new List<T>();
var changed = new List<TDelta>();
foreach (var (id, p) in prevById)
{
if (!nextById.TryGetValue(id, out var n)) { removed.Add(p); continue; }
if (!EqualityComparer<T>.Default.Equals(p, n)) changed.Add(deltaFactory(p, n));
}
foreach (var (id, n) in nextById)
{
if (!prevById.ContainsKey(id)) added.Add(n);
}
added.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
removed.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
return (added, removed, changed);
}
}