refactor(opcuaclient): remove vestigial TargetNamespaceKind + UnsMappingTable (#648)

Nothing read the mapping table beyond the startup validator; v3 UNS projection is
explicit via UnsTagReference. Legacy keys in stored DriverConfig are ignored (pinned).
Reverts the docker-dev opc-plc seed workaround from fdee129f.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 19:14:37 -04:00
parent 8801631739
commit dd05baedd6
16 changed files with 90 additions and 246 deletions
@@ -21,7 +21,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser
// Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions and
// OpcUaClientDriverProbe._opts so the browser parses a given DriverConfig JSON
// the same way as the factory and the probe. The JsonStringEnumConverter lets
// enum-valued knobs (SecurityPolicy / SecurityMode / AuthType / TargetNamespaceKind)
// enum-valued knobs (SecurityPolicy / SecurityMode / AuthType)
// be authored as their string names — the natural form for AdminUI-emitted JSON.
// JsonStringEnumConverter also accepts numeric ordinals, so existing numeric configs
// continue to work without migration.
@@ -153,36 +153,6 @@ public sealed record OpcUaClientDriverOptions
[Range(1, int.MaxValue)]
public int MaxBrowseDepth { get; init; } = 10;
/// <summary>
/// The namespace kind this driver instance gateways into. Per
/// <c>docs/v2/driver-specs.md</c> §8 "Namespace Assignment", the OPC UA Client driver
/// is the only driver that supports <b>either</b> kind, decided per instance:
/// <list type="bullet">
/// <item><see cref="OpcUaTargetNamespaceKind.Equipment"/> — gatewaying a remote
/// server that exposes raw equipment data; the driver remaps remote browse paths
/// to UNS.</item>
/// <item><see cref="OpcUaTargetNamespaceKind.SystemPlatform"/> — gatewaying a
/// remote server that exposes processed/derived data; hierarchy is preserved via
/// the remote browse path with no UNS conversion.</item>
/// </list>
/// The driver enforces the choice at startup — see
/// <c>OpcUaClientDriver.ValidateNamespaceKind</c> — so a misconfiguration fails
/// draft validation rather than surfacing as a runtime surprise.
/// </summary>
public OpcUaTargetNamespaceKind TargetNamespaceKind { get; init; } = OpcUaTargetNamespaceKind.Equipment;
/// <summary>
/// Optional config-driven remote-to-UNS mapping table. Required when
/// <see cref="TargetNamespaceKind"/> is <see cref="OpcUaTargetNamespaceKind.Equipment"/>:
/// §8 mandates that a remote server gatewayed as Equipment carry a mapping table
/// because remote nodes do not conform to UNS by default. Keys are remote browse-path
/// prefixes; values are the UNS Area/Line/Name path the matching subtree maps onto.
/// For <see cref="OpcUaTargetNamespaceKind.SystemPlatform"/> the table must be empty —
/// processed data preserves its own hierarchy and a mapping table would be ambiguous.
/// </summary>
public IReadOnlyDictionary<string, string> UnsMappingTable { get; init; }
= new Dictionary<string, string>();
/// <summary>
/// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
/// 60s server-side maximum; this default is what the form pre-fills for new instances.
@@ -266,25 +236,6 @@ public sealed record OpcUaCertificateValidationOptions
public string? IssuerStorePath { get; init; }
}
/// <summary>
/// The namespace kind an OPC UA Client driver instance gateways its remote server into.
/// See <c>docs/v2/driver-specs.md</c> §8 "Namespace Assignment".
/// </summary>
public enum OpcUaTargetNamespaceKind
{
/// <summary>
/// Remote server exposes raw equipment data; remote browse paths are remapped to UNS
/// via <see cref="OpcUaClientDriverOptions.UnsMappingTable"/>.
/// </summary>
Equipment,
/// <summary>
/// Remote server exposes processed/derived data; hierarchy is preserved via the
/// remote browse path with no UNS conversion.
/// </summary>
SystemPlatform,
}
/// <summary>OPC UA message security mode.</summary>
public enum OpcUaSecurityMode
{
@@ -241,10 +241,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
if (HasConfigBody(driverConfigJson))
_options = OpcUaClientDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson);
// Enforce the Equipment-vs-SystemPlatform choice at startup per driver-specs.md
// §8 "Namespace Assignment" — a misconfigured remote fails draft validation here,
// not as a runtime surprise.
ValidateNamespaceKind(_options);
ValidateTuning(_options);
var appConfig = await BuildApplicationConfigurationAsync(cancellationToken).ConfigureAwait(false);
@@ -643,42 +639,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
return effective < TimeSpan.FromMilliseconds(50) ? 50 : (int)effective.TotalMilliseconds;
}
/// <summary>
/// Enforce the §8 "Namespace Assignment" rule at startup. An <c>Equipment</c>-kind
/// instance gateways raw equipment data and therefore needs a config-driven UNS
/// mapping table (remote nodes don't conform to UNS); a <c>SystemPlatform</c>-kind
/// instance gateways processed data whose hierarchy is preserved verbatim, so a
/// UNS mapping table is meaningless and rejected. Throwing here surfaces the
/// misconfiguration as a draft-validation failure rather than a runtime surprise.
/// </summary>
/// <param name="opts">Driver options containing namespace configuration.</param>
internal static void ValidateNamespaceKind(OpcUaClientDriverOptions opts)
{
switch (opts.TargetNamespaceKind)
{
case OpcUaTargetNamespaceKind.Equipment:
if (opts.UnsMappingTable is null || opts.UnsMappingTable.Count == 0)
throw new InvalidOperationException(
"OpcUaClient driver configured with TargetNamespaceKind=Equipment but no " +
"UnsMappingTable: §8 requires a config-driven remote-to-UNS mapping table " +
"because remote nodes do not conform to UNS by default. Provide a mapping " +
"table or set TargetNamespaceKind=SystemPlatform if the remote exposes " +
"processed data.");
break;
case OpcUaTargetNamespaceKind.SystemPlatform:
if (opts.UnsMappingTable is { Count: > 0 })
throw new InvalidOperationException(
"OpcUaClient driver configured with TargetNamespaceKind=SystemPlatform but " +
"a UnsMappingTable was supplied: processed data preserves its own hierarchy " +
"and a UNS mapping table is ambiguous here. Clear the mapping table or set " +
"TargetNamespaceKind=Equipment if the remote exposes raw equipment data.");
break;
default:
throw new ArgumentOutOfRangeException(
nameof(opts), opts.TargetNamespaceKind, "Unknown TargetNamespaceKind.");
}
}
/// <summary>
/// Build the user-identity token from the driver options. Split out of
/// <see cref="InitializeAsync"/> so the failover sweep reuses one identity across
@@ -23,7 +23,7 @@ public static class OpcUaClientDriverFactoryExtensions
// Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
// The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
// AuthType / TargetNamespaceKind) be authored as their string names — the natural form
// AuthType) be authored as their string names — the natural form
// for human-edited + AdminUI-emitted DriverConfig JSON.
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -161,30 +161,6 @@
</div>
</section>
@* Namespace mapping *@
<section class="panel rise mt-3" style="animation-delay:.12s">
<div class="panel-head">Namespace mapping</div>
<div style="padding:1rem">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Target namespace kind</label>
<InputSelect @bind-Value="_form.OpcUa.TargetNamespaceKind" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
@foreach (var e in Enum.GetValues<OpcUaTargetNamespaceKind>())
{
<option value="@e">@e</option>
}
</InputSelect>
<div class="form-text">Equipment = raw data re-mapped to UNS. SystemPlatform = processed data; hierarchy preserved as-is.</div>
</div>
<div class="col-12">
<label class="form-label">UNS mapping table (read-only — edit via raw JSON import)</label>
<pre class="form-control form-control-sm mono" style="min-height:3rem;overflow:auto;white-space:pre-wrap;">@_unsMappingTableJson</pre>
<div class="form-text">Keys = remote browse-path prefixes; values = UNS Area/Line/Name paths. Required when TargetNamespaceKind = Equipment.</div>
</div>
</div>
</div>
</section>
@* Subscriptions, limits, revocation, discovery (#273/#275/#277/#278) *@
<section class="panel rise mt-3" style="animation-delay:.13s">
<div class="panel-head">Subscriptions, limits &amp; discovery</div>
@@ -272,7 +248,6 @@
private FormModel _form = new();
private List<EndpointUrlRow> _endpoints = [];
private string _unsMappingTableJson = "{}";
private string? _lastParsed;
protected override void OnParametersSet()
@@ -282,7 +257,6 @@
var opts = TryDeserialize(DriverConfigJson) ?? new OpcUaClientDriverOptions();
_form = new FormModel { OpcUa = OpcUaClientFormModel.FromRecord(opts) };
_endpoints = opts.EndpointUrls.Select(EndpointUrlRow.FromUrl).ToList();
_unsMappingTableJson = JsonSerializer.Serialize(opts.UnsMappingTable, _jsonOpts);
_lastParsed = DriverConfigJson;
}
}
@@ -369,7 +343,6 @@
public string? UserCertificatePath { get; set; }
public string? UserCertificatePassword { get; set; }
public OpcUaTargetNamespaceKind TargetNamespaceKind { get; set; } = OpcUaTargetNamespaceKind.Equipment;
public int ProbeTimeoutSeconds { get; set; } = 15;
@@ -385,7 +358,6 @@
public string? DiscoveryUrl { get; set; }
public string? DiscoveryServerUri { get; set; }
internal IReadOnlyDictionary<string, string> _unsMappingTable = new Dictionary<string, string>();
public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new()
{
@@ -408,7 +380,6 @@
Password = r.Password,
UserCertificatePath = r.UserCertificatePath,
UserCertificatePassword = r.UserCertificatePassword,
TargetNamespaceKind = r.TargetNamespaceKind,
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
SubscriptionPublishingIntervalMs = r.SubscriptionPublishingInterval is { } spi ? (int)spi.TotalMilliseconds : null,
SubscriptionKeepAliveCount = (int)Math.Min(r.SubscriptionKeepAliveCount, int.MaxValue),
@@ -419,7 +390,6 @@
IssuerStorePath = r.CertificateValidation.IssuerStorePath,
DiscoveryUrl = r.DiscoveryUrl,
DiscoveryServerUri = r.DiscoveryServerUri,
_unsMappingTable = r.UnsMappingTable,
};
public OpcUaClientDriverOptions ToRecord(IReadOnlyList<string> endpointUrls) => new()
@@ -444,8 +414,6 @@
Password = string.IsNullOrWhiteSpace(Password) ? null : Password,
UserCertificatePath = string.IsNullOrWhiteSpace(UserCertificatePath) ? null : UserCertificatePath,
UserCertificatePassword = string.IsNullOrWhiteSpace(UserCertificatePassword) ? null : UserCertificatePassword,
TargetNamespaceKind = TargetNamespaceKind,
UnsMappingTable = _unsMappingTable,
ProbeTimeoutSeconds = ProbeTimeoutSeconds,
SubscriptionPublishingInterval = SubscriptionPublishingIntervalMs is { } ms and > 0
? TimeSpan.FromMilliseconds(ms) : null,