feat(validation): semantic checks for List attributes (element type, default value, trigger operands)

This commit is contained in:
Joseph Doherty
2026-06-16 15:38:18 -04:00
parent a1d464b50d
commit 872ce2b565
3 changed files with 295 additions and 1 deletions
@@ -1,4 +1,6 @@
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
@@ -48,6 +50,11 @@ public class SemanticValidator
attributeMap.TryAdd(a.CanonicalName, a);
}
// List-attribute type semantics (MV-5): element-type cardinality + default
// value parseability. Trigger-operand rejection (rule 3) is handled below
// by the existing NumericDataTypes guard (List is never numeric).
ValidateListAttributes(configuration, errors);
// Collect alarm on-trigger script names for cross-call violation checks
var alarmOnTriggerScripts = new HashSet<string>(StringComparer.Ordinal);
foreach (var alarm in configuration.Alarms)
@@ -250,6 +257,77 @@ public class SemanticValidator
return new ValidationResult { Errors = errors, Warnings = warnings };
}
/// <summary>
/// MV-5 — semantic validation of List-attribute type configuration. Two rules:
/// <list type="number">
/// <item><b>Element-type cardinality.</b> A <see cref="DataType.List"/> attribute
/// must carry a non-empty <see cref="ResolvedAttribute.ElementDataType"/> that is
/// a valid element scalar (see <see cref="AttributeValueCodec.IsValidElementType"/>);
/// a non-List attribute must NOT carry an element type.</item>
/// <item><b>Default-value parseability.</b> A non-empty authored default
/// <see cref="ResolvedAttribute.Value"/> on a List attribute must
/// <see cref="AttributeValueCodec.Decode"/> without throwing.</item>
/// </list>
/// Attributes whose <see cref="ResolvedAttribute.DataType"/> doesn't parse to a
/// known <see cref="DataType"/> are skipped here (their data type is not "List",
/// so only the "no element type" half could apply, and an unparseable type is a
/// separate concern not introduced by this feature).
/// </summary>
private static void ValidateListAttributes(
FlattenedConfiguration configuration,
List<ValidationEntry> errors)
{
foreach (var attr in configuration.Attributes)
{
var isList = string.Equals(attr.DataType, nameof(DataType.List), StringComparison.OrdinalIgnoreCase);
var hasElementType = !string.IsNullOrWhiteSpace(attr.ElementDataType);
// ── Rule 1: element-type cardinality ─────────────────────────────
if (!isList)
{
if (hasElementType)
{
errors.Add(ValidationEntry.Error(ValidationCategory.MissingMetadata,
$"Attribute '{attr.CanonicalName}' has data type '{attr.DataType}' but declares an element type '{attr.ElementDataType}'; element types are only valid on List attributes.",
attr.CanonicalName));
}
continue; // Non-List attributes have no list-specific value to check.
}
if (!hasElementType)
{
errors.Add(ValidationEntry.Error(ValidationCategory.MissingMetadata,
$"List attribute '{attr.CanonicalName}' must declare an element type (one of String, Int32, Float, Double, Boolean, DateTime).",
attr.CanonicalName));
continue; // Without an element type we can't validate the default value.
}
if (!Enum.TryParse<DataType>(attr.ElementDataType, ignoreCase: true, out var elementType)
|| !AttributeValueCodec.IsValidElementType(elementType))
{
errors.Add(ValidationEntry.Error(ValidationCategory.MissingMetadata,
$"List attribute '{attr.CanonicalName}' has element type '{attr.ElementDataType}', which is not a valid element scalar (one of String, Int32, Float, Double, Boolean, DateTime).",
attr.CanonicalName));
continue; // A bad element type makes the default-value check meaningless.
}
// ── Rule 2: default-value parseability ───────────────────────────
if (!string.IsNullOrEmpty(attr.Value))
{
try
{
AttributeValueCodec.Decode(attr.Value, DataType.List, elementType);
}
catch (FormatException ex)
{
errors.Add(ValidationEntry.Error(ValidationCategory.MissingMetadata,
$"List attribute '{attr.CanonicalName}' has a default value that is not a valid list of '{elementType}': {ex.Message}",
attr.CanonicalName));
}
}
}
}
private static void ValidateCallParameters(
string callerName,
CallTarget call,
@@ -18,7 +18,10 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
/// and (on the deploy path) the bound connection must exist on the target site.
/// Severity is context-dependent: a non-blocking Warning at template design time
/// (bindings are set later) and a deploy-gating Error when enforced (M2.8 / #23).
/// 8. Does NOT verify tag path resolution on devices
/// 8. List-attribute type semantics (MV-5, via <see cref="SemanticValidator"/>):
/// element-type cardinality, default-value parseability, and trigger-operand
/// rejection for List attributes.
/// 9. Does NOT verify tag path resolution on devices
/// </summary>
public class ValidationService
{