using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply; /// /// Per-entity diff computed locally on the node. The enumerable order matches the dependency /// order expected by : namespace → driver → device → equipment → /// poll group → tag → ACL, with Removed processed before Added inside each bucket so cascades /// settle before new rows appear. /// public sealed record GenerationDiff( IReadOnlyList> Namespaces, IReadOnlyList> Drivers, IReadOnlyList> Devices, IReadOnlyList> Equipment, IReadOnlyList> PollGroups, IReadOnlyList> Tags); public sealed record EntityChange(ChangeKind Kind, string LogicalId, T? From, T? To); public static class GenerationDiffer { public static GenerationDiff Compute(DraftSnapshot? from, DraftSnapshot to) { from ??= new DraftSnapshot { GenerationId = 0, ClusterId = to.ClusterId }; return new GenerationDiff( Namespaces: DiffById(from.Namespaces, to.Namespaces, x => x.NamespaceId, (a, b) => (a.ClusterId, a.NamespaceUri, a.Kind, a.Enabled, a.Notes) == (b.ClusterId, b.NamespaceUri, b.Kind, b.Enabled, b.Notes)), Drivers: DiffById(from.DriverInstances, to.DriverInstances, x => x.DriverInstanceId, (a, b) => (a.ClusterId, a.NamespaceId, a.Name, a.DriverType, a.Enabled, a.DriverConfig) == (b.ClusterId, b.NamespaceId, b.Name, b.DriverType, b.Enabled, b.DriverConfig)), Devices: DiffById(from.Devices, to.Devices, x => x.DeviceId, (a, b) => (a.DriverInstanceId, a.Name, a.Enabled, a.DeviceConfig) == (b.DriverInstanceId, b.Name, b.Enabled, b.DeviceConfig)), Equipment: DiffById(from.Equipment, to.Equipment, x => x.EquipmentId, (a, b) => (a.EquipmentUuid, a.DriverInstanceId, a.UnsLineId, a.Name, a.MachineCode, a.ZTag, a.SAPID, a.Enabled) == (b.EquipmentUuid, b.DriverInstanceId, b.UnsLineId, b.Name, b.MachineCode, b.ZTag, b.SAPID, b.Enabled)), PollGroups: DiffById(from.PollGroups, to.PollGroups, x => x.PollGroupId, (a, b) => (a.DriverInstanceId, a.Name, a.IntervalMs) == (b.DriverInstanceId, b.Name, b.IntervalMs)), Tags: DiffById(from.Tags, to.Tags, x => x.TagId, (a, b) => (a.DriverInstanceId, a.DeviceId, a.EquipmentId, a.PollGroupId, a.FolderPath, a.Name, a.DataType, a.AccessLevel, a.WriteIdempotent, a.TagConfig) == (b.DriverInstanceId, b.DeviceId, b.EquipmentId, b.PollGroupId, b.FolderPath, b.Name, b.DataType, b.AccessLevel, b.WriteIdempotent, b.TagConfig))); } private static List> DiffById( IReadOnlyList from, IReadOnlyList to, Func id, Func equal) { var fromById = from.ToDictionary(id); var toById = to.ToDictionary(id); var result = new List>(); foreach (var (logicalId, src) in fromById.Where(kv => !toById.ContainsKey(kv.Key))) result.Add(new(ChangeKind.Removed, logicalId, src, default)); foreach (var (logicalId, dst) in toById) { if (!fromById.TryGetValue(logicalId, out var src)) result.Add(new(ChangeKind.Added, logicalId, default, dst)); else if (!equal(src, dst)) result.Add(new(ChangeKind.Modified, logicalId, src, dst)); } return result; } }