feat(templateengine): flatten native alarm sources (inherit/compose/override)
This commit is contained in:
@@ -96,6 +96,13 @@ public class FlatteningService
|
||||
// Step 7: Resolve alarm on-trigger script references to canonical names
|
||||
ResolveAlarmScriptReferences(alarms, alarmScriptIds, scriptCanonicalById);
|
||||
|
||||
// Step 7b: Resolve native alarm source bindings (connection + source
|
||||
// reference). Conditions under each source are discovered at runtime;
|
||||
// flattening only resolves the binding through inherit/compose/override.
|
||||
var nativeAlarmSources = ResolveInheritedNativeAlarmSources(templateChain, prefix: null);
|
||||
ResolveComposedNativeAlarmSources(templateChain, compositionMap, composedTemplateChains, nativeAlarmSources);
|
||||
ApplyInstanceNativeAlarmSourceOverrides(instance.NativeAlarmSourceOverrides, nativeAlarmSources);
|
||||
|
||||
// Step 8: Collect connection configurations for deployment packaging
|
||||
var connections = new Dictionary<string, ConnectionConfig>();
|
||||
foreach (var attr in attributes.Values)
|
||||
@@ -125,6 +132,7 @@ public class FlatteningService
|
||||
AreaId = instance.AreaId,
|
||||
Attributes = attributes.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||
Alarms = alarms.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||
NativeAlarmSources = nativeAlarmSources.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||
Scripts = scripts.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
|
||||
Connections = connections.Count > 0 ? connections : null,
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow
|
||||
@@ -649,6 +657,137 @@ public class FlatteningService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves native alarm source bindings from an inheritance chain. Keys of
|
||||
/// the returned dictionary are bare binding names (caller path-qualifies for
|
||||
/// composed modules). Derived templates win unless the base binding is
|
||||
/// locked; <see cref="TemplateNativeAlarmSource.IsInherited"/> placeholders
|
||||
/// never shadow a live base binding.
|
||||
/// </summary>
|
||||
private static Dictionary<string, ResolvedNativeAlarmSource> ResolveInheritedNativeAlarmSources(
|
||||
IReadOnlyList<Template> templateChain,
|
||||
string? prefix)
|
||||
{
|
||||
var result = new Dictionary<string, ResolvedNativeAlarmSource>(StringComparer.Ordinal);
|
||||
// Tracks bindings the base locked, so derived templates cannot override them.
|
||||
var lockedNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
for (int i = templateChain.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var template = templateChain[i];
|
||||
var source = i == 0 ? "Template" : "Inherited";
|
||||
|
||||
foreach (var binding in template.NativeAlarmSources)
|
||||
{
|
||||
if (result.ContainsKey(binding.Name))
|
||||
{
|
||||
if (lockedNames.Contains(binding.Name))
|
||||
continue;
|
||||
// IsInherited rows on a derived template are placeholders that
|
||||
// must not shadow the live base binding.
|
||||
if (binding.IsInherited)
|
||||
continue;
|
||||
}
|
||||
|
||||
result[binding.Name] = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = binding.Name,
|
||||
ConnectionName = binding.ConnectionName,
|
||||
SourceReference = binding.SourceReference,
|
||||
ConditionFilter = binding.ConditionFilter,
|
||||
Source = source
|
||||
};
|
||||
|
||||
if (binding.IsLocked)
|
||||
lockedNames.Add(binding.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ResolveComposedNativeAlarmSources(
|
||||
IReadOnlyList<Template> templateChain,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
|
||||
Dictionary<string, ResolvedNativeAlarmSource> sources)
|
||||
{
|
||||
foreach (var template in templateChain)
|
||||
{
|
||||
if (!compositionMap.TryGetValue(template.Id, out var compositions))
|
||||
continue;
|
||||
|
||||
foreach (var composition in compositions)
|
||||
ResolveComposedNativeAlarmSourcesRecursive(
|
||||
composition, composition.InstanceName,
|
||||
compositionMap, composedTemplateChains, sources, new HashSet<int>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively resolves native alarm source bindings of a composed module
|
||||
/// and nested modules, path-qualifying each canonical name with the
|
||||
/// accumulated <paramref name="prefix"/>. The source reference is a raw
|
||||
/// connection address, so (unlike alarm trigger configs) it is not rewritten.
|
||||
/// </summary>
|
||||
private static void ResolveComposedNativeAlarmSourcesRecursive(
|
||||
TemplateComposition composition,
|
||||
string prefix,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
|
||||
Dictionary<string, ResolvedNativeAlarmSource> sources,
|
||||
HashSet<int> visited)
|
||||
{
|
||||
if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
|
||||
return;
|
||||
|
||||
var composedSources = ResolveInheritedNativeAlarmSources(composedChain, prefix);
|
||||
foreach (var (name, binding) in composedSources)
|
||||
{
|
||||
var canonicalName = $"{prefix}.{name}";
|
||||
if (!sources.ContainsKey(canonicalName))
|
||||
{
|
||||
sources[canonicalName] = binding with
|
||||
{
|
||||
CanonicalName = canonicalName,
|
||||
Source = "Composed"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var composedTemplate in composedChain)
|
||||
{
|
||||
if (!visited.Add(composedTemplate.Id))
|
||||
continue;
|
||||
if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
|
||||
continue;
|
||||
|
||||
foreach (var nested in nestedCompositions)
|
||||
ResolveComposedNativeAlarmSourcesRecursive(
|
||||
nested, $"{prefix}.{nested.InstanceName}",
|
||||
compositionMap, composedTemplateChains, sources, visited);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyInstanceNativeAlarmSourceOverrides(
|
||||
ICollection<InstanceNativeAlarmSourceOverride> overrides,
|
||||
Dictionary<string, ResolvedNativeAlarmSource> sources)
|
||||
{
|
||||
foreach (var ovr in overrides)
|
||||
{
|
||||
if (!sources.TryGetValue(ovr.SourceCanonicalName, out var existing))
|
||||
continue; // Cannot add new bindings via overrides.
|
||||
|
||||
sources[ovr.SourceCanonicalName] = existing with
|
||||
{
|
||||
ConnectionName = ovr.ConnectionNameOverride ?? existing.ConnectionName,
|
||||
SourceReference = ovr.SourceReferenceOverride ?? existing.SourceReference,
|
||||
ConditionFilter = ovr.ConditionFilterOverride ?? existing.ConditionFilter,
|
||||
Source = "Override"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves scripts from an inheritance chain. The returned dictionary is
|
||||
/// keyed by bare script name. <paramref name="scriptCanonicalById"/> is
|
||||
|
||||
Reference in New Issue
Block a user