refactor(opcuaserver): composer parses TagConfig once per tag via TagConfigIntent (R2-11, 01/P-1)
Deletes the composer's ExtractTagFullName/Alarm/Historize/Array statics and (co-located to keep the build green, per plan T5) the three OpcUaServer.Tests ExtractTag* suites whose tables now live in Commons.Tests/TagConfigIntentTests. T10 is the grep-sweep verification.
This commit is contained in:
@@ -415,8 +415,11 @@ public static class AddressSpaceComposer
|
||||
.ThenBy(t => t.Name, StringComparer.Ordinal)
|
||||
.Select(t =>
|
||||
{
|
||||
var (isHistorized, historianTagname) = ExtractTagHistorize(t.TagConfig);
|
||||
var (isArray, arrayLength) = ExtractTagArray(t.TagConfig);
|
||||
// Parse the schemaless TagConfig blob ONCE per tag (01/P-1) via the shared byte-parity
|
||||
// authority (01/C-1). TagConfigIntent.Parse is the SINGLE SOURCE OF TRUTH the
|
||||
// artifact-decode seam (DeploymentArtifact), the draft gate (DraftValidator), and the
|
||||
// walker all consume — so the live-compose and artifact-decode plans stay byte-equal.
|
||||
var intent = TagConfigIntent.Parse(t.TagConfig);
|
||||
return new EquipmentTagPlan(
|
||||
TagId: t.TagId,
|
||||
EquipmentId: t.EquipmentId!,
|
||||
@@ -424,13 +427,13 @@ public static class AddressSpaceComposer
|
||||
FolderPath: t.FolderPath ?? string.Empty,
|
||||
Name: t.Name,
|
||||
DataType: t.DataType,
|
||||
FullName: ExtractTagFullName(t.TagConfig),
|
||||
FullName: intent.FullName,
|
||||
Writable: t.AccessLevel == TagAccessLevel.ReadWrite,
|
||||
Alarm: ExtractTagAlarm(t.TagConfig),
|
||||
IsHistorized: isHistorized,
|
||||
HistorianTagname: historianTagname,
|
||||
IsArray: isArray,
|
||||
ArrayLength: arrayLength);
|
||||
Alarm: intent.Alarm is { } a ? new EquipmentTagAlarmInfo(a.AlarmType, a.Severity, a.HistorizeToAveva) : null,
|
||||
IsHistorized: intent.IsHistorized,
|
||||
HistorianTagname: intent.HistorianTagname,
|
||||
IsArray: intent.IsArray,
|
||||
ArrayLength: intent.ArrayLength);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -523,33 +526,6 @@ public static class AddressSpaceComposer
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the driver-side full reference from a <see cref="Tag.TagConfig"/> JSON blob: the
|
||||
/// <c>CK_Tag_TagConfig_IsJson</c> constraint guarantees a JSON object, and every shipped
|
||||
/// driver stores the wire-level address in a top-level <c>FullName</c> field. Replicated from
|
||||
/// <c>EquipmentNodeWalker.ExtractFullName</c> because OpcUaServer does not reference the Core
|
||||
/// driver assembly (kept in sync with the artifact-decode copy in <c>DeploymentArtifact</c>).
|
||||
/// Falls back to the raw blob when it is not a JSON object with a string <c>FullName</c>.
|
||||
/// </summary>
|
||||
/// <param name="tagConfig">The tag's wire-level address JSON.</param>
|
||||
/// <returns>The extracted full reference, or the raw blob when no <c>FullName</c> is present.</returns>
|
||||
private static string ExtractTagFullName(string tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("FullName", out var fullName)
|
||||
&& fullName.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return fullName.GetString() ?? tagConfig;
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* fall through to raw blob */ }
|
||||
return tagConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract a <see cref="Device"/>'s connection host from its schemaless <c>DeviceConfig</c> JSON:
|
||||
/// the top-level <c>"HostAddress"</c> string (e.g. <c>"10.201.31.5:8193"</c>) — the same value a
|
||||
@@ -593,95 +569,4 @@ public static class AddressSpaceComposer
|
||||
/// <returns>The normalized host (trimmed + lower-cased).</returns>
|
||||
public static string NormalizeDeviceHost(string host) => host.Trim().ToLowerInvariant();
|
||||
|
||||
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
|
||||
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
|
||||
/// artifact-decode side (<c>DeploymentArtifact.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed native-alarm intent, or <see langword="null"/> when the tag has no alarm object.</returns>
|
||||
internal static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
if (!doc.RootElement.TryGetProperty("alarm", out var a) || a.ValueKind != JsonValueKind.Object) return null;
|
||||
var type = a.TryGetProperty("alarmType", out var tEl) && tEl.ValueKind == JsonValueKind.String
|
||||
? (tEl.GetString() ?? "AlarmCondition") : "AlarmCondition";
|
||||
var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number
|
||||
&& sEl.TryGetInt32(out var sv) ? sv : 500;
|
||||
// historizeToAveva (bool?, absent ⇒ null ⇒ historize): only an explicit false suppresses the
|
||||
// durable AVEVA write at the HistorianAdapterActor gate; a non-bool node ⇒ null (default-on).
|
||||
bool? historize = a.TryGetProperty("historizeToAveva", out var hEl)
|
||||
&& hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
? hEl.GetBoolean()
|
||||
: null;
|
||||
return new EquipmentTagAlarmInfo(type, sev, historize);
|
||||
}
|
||||
catch (JsonException) { return null; }
|
||||
}
|
||||
|
||||
/// <summary>Parses the optional server-side HistoryRead intent from a tag's <c>TagConfig</c> JSON:
|
||||
/// the <c>isHistorized</c> bool (absent / not a bool / non-object root / blank / malformed ⇒
|
||||
/// <c>false</c>) and the optional <c>historianTagname</c> string override (absent / not a string /
|
||||
/// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName,
|
||||
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
|
||||
/// <c>ExtractTagAlarm</c>. Never throws. The artifact-decode side
|
||||
/// (<c>DeploymentArtifact.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed historize flag and optional historian tagname override.</returns>
|
||||
internal static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return (false, null);
|
||||
var isHistorized = doc.RootElement.TryGetProperty("isHistorized", out var hEl)
|
||||
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
|
||||
&& hEl.GetBoolean();
|
||||
string? tagname = null;
|
||||
if (doc.RootElement.TryGetProperty("historianTagname", out var nEl)
|
||||
&& nEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var raw = nEl.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(raw)) tagname = raw;
|
||||
}
|
||||
return (isHistorized, tagname);
|
||||
}
|
||||
catch (JsonException) { return (false, null); }
|
||||
}
|
||||
|
||||
/// <summary>Parses the optional array intent from a tag's <c>TagConfig</c> JSON: the <c>isArray</c>
|
||||
/// bool (absent / not a bool / non-object root / blank / malformed ⇒ <c>false</c>) and the optional
|
||||
/// <c>arrayLength</c> uint. The length is honoured ONLY when <c>isArray</c> is true AND the prop is a
|
||||
/// JSON number that fits <c>uint</c> (else <c>null</c> ⇒ unbounded 1-D array,
|
||||
/// <c>ArrayDimensions=[0]</c> at materialisation). Mirrors
|
||||
/// <see cref="ExtractTagHistorize"/> exactly in structure + null/blank/non-object/malformed-JSON
|
||||
/// tolerance. Never throws. The artifact-decode side
|
||||
/// (<c>DeploymentArtifact.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed array flag and optional array length.</returns>
|
||||
internal static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return (false, null);
|
||||
var isArray = doc.RootElement.TryGetProperty("isArray", out var aEl)
|
||||
&& (aEl.ValueKind == JsonValueKind.True || aEl.ValueKind == JsonValueKind.False)
|
||||
&& aEl.GetBoolean();
|
||||
uint? arrayLength = null;
|
||||
if (isArray
|
||||
&& doc.RootElement.TryGetProperty("arrayLength", out var lEl)
|
||||
&& lEl.ValueKind == JsonValueKind.Number
|
||||
&& lEl.TryGetUInt32(out var len))
|
||||
{
|
||||
arrayLength = len;
|
||||
}
|
||||
return (isArray, arrayLength);
|
||||
}
|
||||
catch (JsonException) { return (false, null); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user