feat(drivers): shared EquipmentTagRefResolver (by-name + parse-on-miss + cache)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a driver subscription/read/write <c>fullReference</c> to a driver tag-definition,
|
||||
/// bridging the two authoring models: a legacy authored tag-table entry (looked up by name)
|
||||
/// OR an equipment tag whose reference is its raw <c>TagConfig</c> JSON (parsed on first use
|
||||
/// and cached). Negative results are cached too, so a genuinely-unknown reference is parsed once.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDef">The driver's internal tag-definition type.</typeparam>
|
||||
public sealed class EquipmentTagRefResolver<TDef> where TDef : class
|
||||
{
|
||||
private readonly Func<string, TDef?> _byName;
|
||||
private readonly Func<string, TDef?> _parseRef;
|
||||
private readonly ConcurrentDictionary<string, TDef?> _cache = new(StringComparer.Ordinal);
|
||||
|
||||
/// <param name="byName">Authored tag-table lookup (returns null on miss).</param>
|
||||
/// <param name="parseRef">Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null.</param>
|
||||
public EquipmentTagRefResolver(Func<string, TDef?> byName, Func<string, TDef?> parseRef)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(byName);
|
||||
ArgumentNullException.ThrowIfNull(parseRef);
|
||||
_byName = byName;
|
||||
_parseRef = parseRef;
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="fullReference"/> resolves to a def (authored or equipment).</summary>
|
||||
/// <param name="fullReference">The wire reference handed to the driver.</param>
|
||||
/// <param name="def">The resolved tag-definition when this returns true.</param>
|
||||
/// <returns><see langword="true"/> when a definition was found.</returns>
|
||||
public bool TryResolve(string fullReference, out TDef def)
|
||||
{
|
||||
var authored = _byName(fullReference);
|
||||
if (authored is not null) { def = authored; return true; }
|
||||
|
||||
var resolved = _cache.GetOrAdd(fullReference, _parseRef);
|
||||
if (resolved is not null) { def = resolved; return true; }
|
||||
|
||||
def = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Drops the transient-parse cache (call on driver reinitialise so a config change re-parses).</summary>
|
||||
public void Clear() => _cache.Clear();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
public class EquipmentTagRefResolverTests
|
||||
{
|
||||
private sealed record Def(string Id);
|
||||
|
||||
private static EquipmentTagRefResolver<Def> Make(
|
||||
Dictionary<string, Def> byName, Func<string, Def?> parse)
|
||||
=> new(r => byName.TryGetValue(r, out var d) ? d : null, parse);
|
||||
|
||||
[Fact]
|
||||
public void Legacy_name_resolves_via_byName()
|
||||
{
|
||||
var r = Make(new() { ["Temp"] = new Def("Temp") }, _ => null);
|
||||
r.TryResolve("Temp", out var def).ShouldBeTrue();
|
||||
def!.Id.ShouldBe("Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equipment_ref_resolves_via_parse_and_is_cached()
|
||||
{
|
||||
var calls = 0;
|
||||
var r = Make(new(), reference => { calls++; return reference.StartsWith("{") ? new Def(reference) : null; });
|
||||
r.TryResolve("{\"a\":1}", out var d1).ShouldBeTrue();
|
||||
r.TryResolve("{\"a\":1}", out var d2).ShouldBeTrue();
|
||||
d1!.Id.ShouldBe("{\"a\":1}");
|
||||
calls.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Garbage_ref_returns_false_and_caches_negative()
|
||||
{
|
||||
var calls = 0;
|
||||
var r = Make(new(), _ => { calls++; return null; });
|
||||
r.TryResolve("nope", out _).ShouldBeFalse();
|
||||
r.TryResolve("nope", out _).ShouldBeFalse();
|
||||
calls.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_drops_transient_cache()
|
||||
{
|
||||
var calls = 0;
|
||||
var r = Make(new(), reference => { calls++; return new Def(reference); });
|
||||
r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
|
||||
r.Clear();
|
||||
r.TryResolve("{\"a\":1}", out _).ShouldBeTrue();
|
||||
calls.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user