- WP-23: ITemplateEngineRepository full EF Core implementation - WP-1: Template CRUD with deletion constraints (instances, children, compositions) - WP-2–4: Attribute, alarm, script definitions with lock flags and override granularity - WP-5: Shared script CRUD with syntax validation - WP-6–7: Composition with recursive nesting and canonical naming - WP-8–11: Override granularity, locking rules, inheritance/composition scope - WP-12: Naming collision detection on canonical names (recursive) - WP-13: Graph acyclicity (inheritance + composition cycles) Core services: TemplateService, SharedScriptService, TemplateResolver, LockEnforcer, CollisionDetector, CycleDetector. 358 tests pass.
156 lines
5.7 KiB
C#
156 lines
5.7 KiB
C#
using ScadaLink.Commons.Entities.Templates;
|
|
|
|
namespace ScadaLink.TemplateEngine;
|
|
|
|
/// <summary>
|
|
/// Walks inheritance and composition chains to resolve effective template members.
|
|
/// Produces canonical (path-qualified) names for composed module members.
|
|
/// </summary>
|
|
public static class TemplateResolver
|
|
{
|
|
/// <summary>
|
|
/// Represents a resolved member from any point in the inheritance/composition hierarchy.
|
|
/// </summary>
|
|
public sealed record ResolvedTemplateMember
|
|
{
|
|
public string CanonicalName { get; init; }
|
|
public string MemberType { get; init; } // "Attribute", "Alarm", "Script"
|
|
public int SourceTemplateId { get; init; }
|
|
public int MemberId { get; init; }
|
|
public bool IsLocked { get; init; }
|
|
public string? ModulePath { get; init; }
|
|
|
|
public ResolvedTemplateMember(string canonicalName, string memberType, int sourceTemplateId, int memberId, bool isLocked, string? modulePath = null)
|
|
{
|
|
CanonicalName = canonicalName;
|
|
MemberType = memberType;
|
|
SourceTemplateId = sourceTemplateId;
|
|
MemberId = memberId;
|
|
IsLocked = isLocked;
|
|
ModulePath = modulePath;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves all effective members for a template, walking inheritance and composition chains.
|
|
/// Child members override parent members of the same canonical name (unless locked).
|
|
/// </summary>
|
|
public static IReadOnlyList<ResolvedTemplateMember> ResolveAllMembers(
|
|
int templateId,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
var lookup = allTemplates.ToDictionary(t => t.Id);
|
|
if (!lookup.TryGetValue(templateId, out var template))
|
|
return Array.Empty<ResolvedTemplateMember>();
|
|
|
|
// Build inheritance chain from root to leaf (root first, child last)
|
|
var chain = BuildInheritanceChain(templateId, lookup);
|
|
|
|
// Start with root members, apply overrides from each child
|
|
var effectiveMembers = new Dictionary<string, ResolvedTemplateMember>(StringComparer.Ordinal);
|
|
|
|
foreach (var tmpl in chain)
|
|
{
|
|
// Direct members
|
|
AddDirectMembers(tmpl, prefix: null, effectiveMembers);
|
|
|
|
// Composed module members
|
|
foreach (var composition in tmpl.Compositions)
|
|
{
|
|
if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
|
|
{
|
|
AddComposedMembers(composedTemplate, composition.InstanceName, lookup, effectiveMembers, new HashSet<int>());
|
|
}
|
|
}
|
|
}
|
|
|
|
return effectiveMembers.Values.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the inheritance chain from root ancestor to the specified template.
|
|
/// </summary>
|
|
public static IReadOnlyList<Template> BuildInheritanceChain(
|
|
int templateId,
|
|
IReadOnlyDictionary<int, Template> lookup)
|
|
{
|
|
var chain = new List<Template>();
|
|
var currentId = templateId;
|
|
var visited = new HashSet<int>();
|
|
|
|
while (currentId != 0 && lookup.TryGetValue(currentId, out var current))
|
|
{
|
|
if (!visited.Add(currentId))
|
|
break; // Safety: cycle detected
|
|
|
|
chain.Add(current);
|
|
currentId = current.ParentTemplateId ?? 0;
|
|
}
|
|
|
|
chain.Reverse(); // Root first
|
|
return chain;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a member by canonical name in the resolved member set.
|
|
/// Used to check override/lock constraints.
|
|
/// </summary>
|
|
public static ResolvedTemplateMember? FindMemberByCanonicalName(
|
|
string canonicalName,
|
|
int parentTemplateId,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
var members = ResolveAllMembers(parentTemplateId, allTemplates);
|
|
return members.FirstOrDefault(m => m.CanonicalName == canonicalName);
|
|
}
|
|
|
|
private static void AddDirectMembers(
|
|
Template template,
|
|
string? prefix,
|
|
Dictionary<string, ResolvedTemplateMember> effectiveMembers)
|
|
{
|
|
foreach (var attr in template.Attributes)
|
|
{
|
|
var canonicalName = prefix == null ? attr.Name : $"{prefix}.{attr.Name}";
|
|
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
|
|
canonicalName, "Attribute", template.Id, attr.Id, attr.IsLocked, prefix);
|
|
}
|
|
|
|
foreach (var alarm in template.Alarms)
|
|
{
|
|
var canonicalName = prefix == null ? alarm.Name : $"{prefix}.{alarm.Name}";
|
|
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
|
|
canonicalName, "Alarm", template.Id, alarm.Id, alarm.IsLocked, prefix);
|
|
}
|
|
|
|
foreach (var script in template.Scripts)
|
|
{
|
|
var canonicalName = prefix == null ? script.Name : $"{prefix}.{script.Name}";
|
|
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
|
|
canonicalName, "Script", template.Id, script.Id, script.IsLocked, prefix);
|
|
}
|
|
}
|
|
|
|
private static void AddComposedMembers(
|
|
Template template,
|
|
string prefix,
|
|
Dictionary<int, Template> lookup,
|
|
Dictionary<string, ResolvedTemplateMember> effectiveMembers,
|
|
HashSet<int> visited)
|
|
{
|
|
if (!visited.Add(template.Id))
|
|
return;
|
|
|
|
AddDirectMembers(template, prefix, effectiveMembers);
|
|
|
|
foreach (var composition in template.Compositions)
|
|
{
|
|
if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
|
|
{
|
|
var nestedPrefix = $"{prefix}.{composition.InstanceName}";
|
|
AddComposedMembers(nested, nestedPrefix, lookup, effectiveMembers, visited);
|
|
}
|
|
}
|
|
}
|
|
}
|