feat(runtime): node-scoped ParseComposition filters address space by ClusterId
This commit is contained in:
@@ -205,6 +205,91 @@ public static class DeploymentArtifact
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cluster-scoped overload: the address-space composition a node should materialise given
|
||||
/// its NodeId. Filters every projection to the node's own ClusterId (see <see cref="ResolveClusterScope"/>).</summary>
|
||||
/// <param name="blob">The deployment artifact blob.</param>
|
||||
/// <param name="nodeId">This node's identity in "host:port" form.</param>
|
||||
/// <returns>The filtered composition per the node's scoping decision.</returns>
|
||||
public static Phase7CompositionResult ParseComposition(ReadOnlySpan<byte> blob, string nodeId)
|
||||
{
|
||||
var scope = ResolveClusterScope(blob, nodeId);
|
||||
if (scope.Mode == ClusterFilterMode.None) return ParseComposition(blob);
|
||||
if (scope.Mode == ClusterFilterMode.Suppress) return Empty();
|
||||
|
||||
var full = ParseComposition(blob);
|
||||
var sets = BuildClusterSets(blob, scope.ClusterId!);
|
||||
return new Phase7CompositionResult(
|
||||
full.UnsAreas.Where(a => sets.AreaIds.Contains(a.UnsAreaId)).ToArray(),
|
||||
full.UnsLines.Where(l => sets.AreaIds.Contains(l.UnsAreaId)).ToArray(),
|
||||
full.EquipmentNodes.Where(e => sets.EquipmentIds.Contains(e.EquipmentId)).ToArray(),
|
||||
full.DriverInstancePlans.Where(d => sets.DriverIds.Contains(d.DriverInstanceId)).ToArray(),
|
||||
full.ScriptedAlarmPlans.Where(a => sets.EquipmentIds.Contains(a.EquipmentId)).ToArray(),
|
||||
full.GalaxyTags.Where(t => sets.DriverIds.Contains(t.DriverInstanceId)).ToArray())
|
||||
{
|
||||
EquipmentTags = full.EquipmentTags.Where(t => sets.DriverIds.Contains(t.DriverInstanceId)).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>The in-cluster id sets used to filter a composition.</summary>
|
||||
/// <param name="DriverIds">DriverInstanceIds whose row carries the in-scope ClusterId.</param>
|
||||
/// <param name="AreaIds">UnsAreaIds whose row carries the in-scope ClusterId.</param>
|
||||
/// <param name="EquipmentIds">EquipmentIds whose owning DriverInstanceId is in-cluster.</param>
|
||||
private sealed record ClusterSets(HashSet<string> DriverIds, HashSet<string> AreaIds, HashSet<string> EquipmentIds);
|
||||
|
||||
/// <summary>Build the in-cluster id sets used to filter a composition: DriverInstanceIds + UnsAreaIds
|
||||
/// that directly carry the ClusterId, plus EquipmentIds whose DriverInstanceId is in-cluster.</summary>
|
||||
/// <param name="blob">The deployment artifact blob.</param>
|
||||
/// <param name="clusterId">The node's ClusterId to scope to.</param>
|
||||
/// <returns>The resolved in-cluster id sets (empty on parse failure => empty composition).</returns>
|
||||
private static ClusterSets BuildClusterSets(ReadOnlySpan<byte> blob, string clusterId)
|
||||
{
|
||||
var driverIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var areaIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var equipmentIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(blob.ToArray());
|
||||
var root = doc.RootElement;
|
||||
CollectIdsWhereCluster(root, "DriverInstances", "DriverInstanceId", clusterId, driverIds);
|
||||
CollectIdsWhereCluster(root, "UnsAreas", "UnsAreaId", clusterId, areaIds);
|
||||
// Equipment carries no ClusterId — include it when its DriverInstanceId is in-cluster.
|
||||
if (root.TryGetProperty("Equipment", out var eq) && eq.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var el in eq.EnumerateArray())
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object) continue;
|
||||
var di = el.TryGetProperty("DriverInstanceId", out var diEl) ? diEl.GetString() : null;
|
||||
var id = el.TryGetProperty("EquipmentId", out var idEl) ? idEl.GetString() : null;
|
||||
if (!string.IsNullOrWhiteSpace(id) && di is not null && driverIds.Contains(di))
|
||||
equipmentIds.Add(id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* empty sets => nothing matches => empty composition */ }
|
||||
return new ClusterSets(driverIds, areaIds, equipmentIds);
|
||||
}
|
||||
|
||||
/// <summary>Collect each row's <paramref name="idField"/> value from <paramref name="arrayName"/> whose
|
||||
/// ClusterId equals <paramref name="clusterId"/> (case-insensitive, matching the codebase convention).</summary>
|
||||
/// <param name="root">The artifact root element.</param>
|
||||
/// <param name="arrayName">The array property name to scan (e.g. "DriverInstances").</param>
|
||||
/// <param name="idField">The id field to collect from each in-cluster row.</param>
|
||||
/// <param name="clusterId">The ClusterId rows must match.</param>
|
||||
/// <param name="into">The set to collect matching ids into.</param>
|
||||
private static void CollectIdsWhereCluster(
|
||||
JsonElement root, string arrayName, string idField, string clusterId, HashSet<string> into)
|
||||
{
|
||||
if (!root.TryGetProperty(arrayName, out var arr) || arr.ValueKind != JsonValueKind.Array) return;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object) continue;
|
||||
var cid = el.TryGetProperty("ClusterId", out var cEl) ? cEl.GetString() : null;
|
||||
if (!string.Equals(cid, clusterId, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var id = el.TryGetProperty(idField, out var idEl) ? idEl.GetString() : null;
|
||||
if (!string.IsNullOrWhiteSpace(id)) into.Add(id!);
|
||||
}
|
||||
}
|
||||
|
||||
private static Phase7CompositionResult Empty() => new(
|
||||
Array.Empty<UnsAreaProjection>(),
|
||||
Array.Empty<UnsLineProjection>(),
|
||||
|
||||
Reference in New Issue
Block a user