feat(commons): DeviceConfigIntent — device-host extraction + normalization home (R2-11)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
{ "id": "T1", "subject": "Golden TagConfig corpus + compose→encode→decode parity characterization test (Runtime.Tests, green on unmodified tree)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T2", "subject": "FullName-semantics characterization for EquipmentNodeWalker (Core.Tests) + DraftValidator Galaxy rule (Configuration.Tests)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T3", "subject": "TagConfigIntent + TagAlarmIntent in Commons/Types (TDD; port composer semantics verbatim + OpcUaServer ExtractTag* test tables)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T4", "subject": "DeviceConfigIntent (TryExtractHost/NormalizeHost) in Commons/Types (TDD)", "status": "pending", "blockedBy": [] },
|
||||
{ "id": "T4", "subject": "DeviceConfigIntent (TryExtractHost/NormalizeHost) in Commons/Types (TDD)", "status": "completed", "blockedBy": [] },
|
||||
{ "id": "T5", "subject": "AddressSpaceComposer swap: single TagConfigIntent.Parse per tag, delete 4 Extract* statics (P-1 parse-once)", "status": "pending", "blockedBy": ["T1", "T3"] },
|
||||
{ "id": "T6", "subject": "Device-host re-home: composer TryExtractDeviceHost/NormalizeDeviceHost → DeviceConfigIntent; update DriverHostActor + DeploymentArtifact callers", "status": "pending", "blockedBy": ["T4", "T5"] },
|
||||
{ "id": "T7", "subject": "DeploymentArtifact swap: delete 4 private mirrors, parse once per tag element via TagConfigIntent", "status": "pending", "blockedBy": ["T5"] },
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
/// <summary>
|
||||
/// SINGLE SOURCE OF TRUTH for extracting + normalizing a <c>Device</c>'s connection host from its
|
||||
/// schemaless <c>DeviceConfig</c> JSON. Shared by the live-compose seam (<c>AddressSpaceComposer</c>),
|
||||
/// the artifact-decode seam (<c>DeploymentArtifact</c>), and the FixedTree-partition path
|
||||
/// (<c>DriverHostActor</c>) so an <c>EquipmentNode.DeviceHost</c> and a driver-discovered device-host
|
||||
/// folder segment for the same device compare byte-equal. Ported verbatim from the historical
|
||||
/// <c>AddressSpaceComposer.TryExtractDeviceHost</c> / <c>NormalizeDeviceHost</c>.
|
||||
/// </summary>
|
||||
public static class DeviceConfigIntent
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract a <c>Device</c>'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>). Returns
|
||||
/// <see langword="null"/> when the config is blank, not a JSON object, has no string
|
||||
/// <c>HostAddress</c>, or the value is blank/whitespace. Never throws. The returned host is
|
||||
/// deterministically normalized via <see cref="NormalizeHost"/> (trim + lower-case).
|
||||
/// </summary>
|
||||
/// <param name="deviceConfigJson">The device's schemaless <c>DeviceConfig</c> JSON blob.</param>
|
||||
/// <returns>The normalized device host, or <see langword="null"/> when absent/blank/unparseable.</returns>
|
||||
public static string? TryExtractHost(string? deviceConfigJson)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceConfigJson)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(deviceConfigJson);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
if (!doc.RootElement.TryGetProperty("HostAddress", out var hostEl)
|
||||
|| hostEl.ValueKind != JsonValueKind.String) return null;
|
||||
var raw = hostEl.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||
return NormalizeHost(raw);
|
||||
}
|
||||
catch (JsonException) { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The single normalization authority for a device host: trims surrounding whitespace and
|
||||
/// lower-cases (invariant). Idempotent (an already-normalized value is unchanged).
|
||||
/// </summary>
|
||||
/// <param name="host">The raw host string (non-null; a non-empty <c>HostAddress</c> or folder segment).</param>
|
||||
/// <returns>The normalized host (trimmed + lower-cased).</returns>
|
||||
public static string NormalizeHost(string host) => host.Trim().ToLowerInvariant();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins <see cref="DeviceConfigIntent"/> device-host extraction + normalization semantics (ported
|
||||
/// from the historical composer statics; R2-11): null on blank/non-object/non-string/whitespace,
|
||||
/// trim+lower normalization, and idempotence.
|
||||
/// </summary>
|
||||
public sealed class DeviceConfigIntentTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("{\"HostAddress\":\"10.0.0.5:8193\"}", "10.0.0.5:8193")]
|
||||
[InlineData("{\"HostAddress\":\" HOST:8193 \"}", "host:8193")] // trim + lower
|
||||
[InlineData("{\"HostAddress\":\"HOST:8193\",\"other\":1}", "host:8193")]
|
||||
// null-yielding cases
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", null)]
|
||||
[InlineData(" ", null)]
|
||||
[InlineData("not json {", null)]
|
||||
[InlineData("[1,2]", null)] // non-object root
|
||||
[InlineData("{\"HostAddress\":123}", null)] // non-string
|
||||
[InlineData("{\"HostAddress\":\" \"}", null)] // whitespace value
|
||||
[InlineData("{\"other\":\"x\"}", null)] // absent
|
||||
public void TryExtractHost(string? cfg, string? expected)
|
||||
{
|
||||
DeviceConfigIntent.TryExtractHost(cfg).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" HOST:8193 ", "host:8193")]
|
||||
[InlineData("host:8193", "host:8193")]
|
||||
public void NormalizeHost_trims_and_lowercases(string raw, string expected)
|
||||
{
|
||||
DeviceConfigIntent.NormalizeHost(raw).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeHost_is_idempotent()
|
||||
{
|
||||
var once = DeviceConfigIntent.NormalizeHost(" HOST:8193 ");
|
||||
DeviceConfigIntent.NormalizeHost(once).ShouldBe(once);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user