feat(validation): require TagConfig.FullName on Galaxy alias tags; reframe Tag doc
This commit is contained in:
@@ -3,9 +3,11 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One canonical tag (signal) in a cluster's generation. Per decision #110:
|
/// One canonical tag (signal) in a cluster's generation. <see cref="EquipmentId"/> set ⟺ the
|
||||||
/// <see cref="EquipmentId"/> is REQUIRED when the driver is in an Equipment-kind namespace
|
/// tag participates in the Equipment tree, regardless of the driver's namespace kind. A
|
||||||
/// and NULL when in SystemPlatform-kind namespace (Galaxy hierarchy preserved).
|
/// <c>GalaxyMxGateway</c>-bound equipment tag is an <em>alias</em> — a Galaxy attribute surfaced
|
||||||
|
/// under a UNS name, with its Galaxy reference carried in <c>TagConfig.FullName</c>.
|
||||||
|
/// <see cref="EquipmentId"/> is NULL for SystemPlatform mirror tags (FolderPath-scoped).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Tag
|
public sealed class Tag
|
||||||
{
|
{
|
||||||
@@ -30,8 +32,8 @@ public sealed class Tag
|
|||||||
public string? DeviceId { get; set; }
|
public string? DeviceId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required when driver is in Equipment-kind namespace; NULL when in SystemPlatform-kind.
|
/// Set when the tag belongs to an Equipment (driver tag OR Galaxy alias); NULL for
|
||||||
/// Cross-table invariant enforced by sp_ValidateDraft (decision #110).
|
/// SystemPlatform mirror tags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? EquipmentId { get; set; }
|
public string? EquipmentId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,43 @@ public static class DraftValidator
|
|||||||
ValidateEquipmentIdDerivation(draft, errors);
|
ValidateEquipmentIdDerivation(draft, errors);
|
||||||
ValidateDriverNamespaceCompatibility(draft, errors);
|
ValidateDriverNamespaceCompatibility(draft, errors);
|
||||||
ValidateNoEquipmentSignalNameCollision(draft, errors);
|
ValidateNoEquipmentSignalNameCollision(draft, errors);
|
||||||
|
ValidateAliasTagFullName(draft, errors);
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ValidateAliasTagFullName(DraftSnapshot draft, List<ValidationError> errors)
|
||||||
|
{
|
||||||
|
var typeByDriver = draft.DriverInstances
|
||||||
|
.ToDictionary(d => d.DriverInstanceId, d => d.DriverType, StringComparer.Ordinal);
|
||||||
|
foreach (var t in draft.Tags)
|
||||||
|
{
|
||||||
|
if (t.EquipmentId is null) continue;
|
||||||
|
if (!typeByDriver.TryGetValue(t.DriverInstanceId, out var dtype) || dtype != "GalaxyMxGateway")
|
||||||
|
continue;
|
||||||
|
if (string.IsNullOrWhiteSpace(ExtractTagConfigFullName(t.TagConfig)))
|
||||||
|
errors.Add(new("AliasTagMissingReference",
|
||||||
|
$"Alias tag '{t.TagId}' on equipment '{t.EquipmentId}' is missing a Galaxy reference (TagConfig.FullName)",
|
||||||
|
t.TagId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal reader for the top-level "FullName" string in a tag's schemaless TagConfig JSON
|
||||||
|
// (mirrors Phase7Composer.ExtractTagFullName — a small local copy, consistent with this codebase
|
||||||
|
// where the composer keeps its own).
|
||||||
|
private static string? ExtractTagConfigFullName(string? tagConfig)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tagConfig)) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(tagConfig);
|
||||||
|
return doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object
|
||||||
|
&& doc.RootElement.TryGetProperty("FullName", out var fn)
|
||||||
|
&& fn.ValueKind == System.Text.Json.JsonValueKind.String ? fn.GetString() : null;
|
||||||
|
}
|
||||||
|
catch (System.Text.Json.JsonException) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateNoEquipmentSignalNameCollision(DraftSnapshot draft, List<ValidationError> errors)
|
private static void ValidateNoEquipmentSignalNameCollision(DraftSnapshot draft, List<ValidationError> errors)
|
||||||
{
|
{
|
||||||
// Materialiser NodeId key: "{EquipmentId}[/{FolderPath}]/{Name}". Tag (EquipmentId != null) and
|
// Materialiser NodeId key: "{EquipmentId}[/{FolderPath}]/{Name}". Tag (EquipmentId != null) and
|
||||||
|
|||||||
@@ -405,16 +405,21 @@ public sealed class DraftValidatorTests
|
|||||||
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
|
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Tag BuildTag(string? equipmentId, string name, string? folderPath) => new()
|
private static Tag BuildTag(
|
||||||
|
string? equipmentId,
|
||||||
|
string name,
|
||||||
|
string? folderPath,
|
||||||
|
string driverInstanceId = "d",
|
||||||
|
string tagConfig = "{}") => new()
|
||||||
{
|
{
|
||||||
TagId = $"tag-{name}",
|
TagId = $"tag-{name}",
|
||||||
DriverInstanceId = "d",
|
DriverInstanceId = driverInstanceId,
|
||||||
EquipmentId = equipmentId,
|
EquipmentId = equipmentId,
|
||||||
Name = name,
|
Name = name,
|
||||||
FolderPath = folderPath,
|
FolderPath = folderPath,
|
||||||
DataType = "Float",
|
DataType = "Float",
|
||||||
AccessLevel = TagAccessLevel.Read,
|
AccessLevel = TagAccessLevel.Read,
|
||||||
TagConfig = "{}",
|
TagConfig = tagConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
|
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
|
||||||
@@ -426,6 +431,57 @@ public sealed class DraftValidatorTests
|
|||||||
ScriptId = "s-1",
|
ScriptId = "s-1",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
// ValidateAliasTagFullName — Galaxy alias tags must carry TagConfig.FullName
|
||||||
|
// ------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Verifies that an equipment-scoped Tag bound to a GalaxyMxGateway driver whose
|
||||||
|
/// TagConfig has no FullName (an alias with no Galaxy reference) is rejected — it would
|
||||||
|
/// subscribe to nothing.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AliasTag_missing_FullName_is_rejected()
|
||||||
|
{
|
||||||
|
var draft = new DraftSnapshot
|
||||||
|
{
|
||||||
|
GenerationId = 1, ClusterId = "c",
|
||||||
|
DriverInstances = [new DriverInstance { DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1", Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||||
|
Tags = [BuildTag(equipmentId: "eq-1", name: "alias", folderPath: null, driverInstanceId: "d-galaxy", tagConfig: "{}")],
|
||||||
|
};
|
||||||
|
|
||||||
|
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "AliasTagMissingReference" && e.Context == "tag-alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that an equipment-scoped Galaxy alias tag carrying a TagConfig.FullName
|
||||||
|
/// reference is accepted.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AliasTag_with_FullName_is_accepted()
|
||||||
|
{
|
||||||
|
var draft = new DraftSnapshot
|
||||||
|
{
|
||||||
|
GenerationId = 1, ClusterId = "c",
|
||||||
|
DriverInstances = [new DriverInstance { DriverInstanceId = "d-galaxy", ClusterId = "c", NamespaceId = "ns-1", Name = "Galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}" }],
|
||||||
|
Tags = [BuildTag(equipmentId: "eq-1", name: "alias", folderPath: null, driverInstanceId: "d-galaxy", tagConfig: "{\"FullName\":\"X.Y\"}")],
|
||||||
|
};
|
||||||
|
|
||||||
|
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "AliasTagMissingReference");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that an equipment-scoped Tag bound to a NON-Galaxy driver with an empty
|
||||||
|
/// TagConfig is NOT flagged as an alias missing its reference — only GalaxyMxGateway-bound
|
||||||
|
/// equipment tags are aliases.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AliasTag_check_skips_nonGalaxy_equipment_tag()
|
||||||
|
{
|
||||||
|
var draft = new DraftSnapshot
|
||||||
|
{
|
||||||
|
GenerationId = 1, ClusterId = "c",
|
||||||
|
DriverInstances = [new DriverInstance { DriverInstanceId = "d-modbus", ClusterId = "c", NamespaceId = "ns-1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}" }],
|
||||||
|
Tags = [BuildTag(equipmentId: "eq-1", name: "alias", folderPath: null, driverInstanceId: "d-modbus", tagConfig: "{}")],
|
||||||
|
};
|
||||||
|
|
||||||
|
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "AliasTagMissingReference");
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
// Phase 6.3 task #148 part 2 — ValidateClusterTopology
|
// Phase 6.3 task #148 part 2 — ValidateClusterTopology
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user