feat(gateway): add ArrayAddressNormalizer for bare-name array AddItem
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rewrites a bare MXAccess attribute address to its writable array form by appending the
|
||||||
|
/// trailing <c>[]</c> suffix when Galaxy Repository metadata reports the attribute as an array.
|
||||||
|
/// MXAccess requires the <c>[]</c> suffix on the AddItem address for an array attribute to be
|
||||||
|
/// writable; the bare name registers a read-only-ish handle. This is best-effort: when metadata
|
||||||
|
/// is cold, the address is unknown, or the attribute is not an array, the address is returned
|
||||||
|
/// unchanged and no exception is thrown.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ArrayAddressNormalizer(IGalaxyHierarchyCache cache)
|
||||||
|
{
|
||||||
|
private const string ArraySuffix = "[]";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <paramref name="address"/> with a trailing <c>[]</c> appended when Galaxy metadata
|
||||||
|
/// reports it as an array attribute; otherwise returns it unchanged. Never throws.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The MXAccess attribute address to normalize.</param>
|
||||||
|
/// <returns>The normalized address, or the original address when no rewrite applies.</returns>
|
||||||
|
public string Normalize(string address)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
|
{
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.EndsWith(ArraySuffix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Galaxy SQL keys array attributes by their suffixed FullTagReference (e.g. "Obj.Arr[]"),
|
||||||
|
// so probe for the suffixed form to decide whether the bare name is an array.
|
||||||
|
string suffixed = address + ArraySuffix;
|
||||||
|
return cache.Current.Index.TagsByAddress.TryGetValue(suffixed, out GalaxyTagLookup? lookup)
|
||||||
|
&& lookup.Attribute?.IsArray == true
|
||||||
|
? suffixed
|
||||||
|
: address;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
|
||||||
|
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||||
|
using ZB.MOM.WW.MxGateway.Server.Sessions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
|
||||||
|
|
||||||
|
public sealed class ArrayAddressNormalizerTests
|
||||||
|
{
|
||||||
|
/// <summary>Verifies a bare array attribute name gains the trailing array suffix.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Normalize_BareArrayName_AppendsArraySuffix()
|
||||||
|
{
|
||||||
|
ArrayAddressNormalizer normalizer = CreateNormalizer();
|
||||||
|
|
||||||
|
Assert.Equal("Obj.Arr[]", normalizer.Normalize("Obj.Arr"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies an already-suffixed address is returned unchanged.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Normalize_AlreadySuffixed_ReturnsUnchanged()
|
||||||
|
{
|
||||||
|
ArrayAddressNormalizer normalizer = CreateNormalizer();
|
||||||
|
|
||||||
|
Assert.Equal("Obj.Arr[]", normalizer.Normalize("Obj.Arr[]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies a scalar attribute is returned unchanged.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Normalize_ScalarAttribute_ReturnsUnchanged()
|
||||||
|
{
|
||||||
|
ArrayAddressNormalizer normalizer = CreateNormalizer();
|
||||||
|
|
||||||
|
Assert.Equal("Obj.Scalar", normalizer.Normalize("Obj.Scalar"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies an address absent from the cache is returned unchanged.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Normalize_UnknownAddress_ReturnsUnchanged()
|
||||||
|
{
|
||||||
|
ArrayAddressNormalizer normalizer = CreateNormalizer();
|
||||||
|
|
||||||
|
Assert.Equal("Obj.Unknown", normalizer.Normalize("Obj.Unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies null, empty, and whitespace addresses are returned unchanged.</summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void Normalize_BlankAddress_ReturnsUnchanged(string address)
|
||||||
|
{
|
||||||
|
ArrayAddressNormalizer normalizer = CreateNormalizer();
|
||||||
|
|
||||||
|
Assert.Equal(address, normalizer.Normalize(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArrayAddressNormalizer CreateNormalizer()
|
||||||
|
{
|
||||||
|
IReadOnlyList<GalaxyObject> objects =
|
||||||
|
[
|
||||||
|
new GalaxyObject
|
||||||
|
{
|
||||||
|
GobjectId = 1,
|
||||||
|
TagName = "Obj",
|
||||||
|
ContainedName = "Obj",
|
||||||
|
Attributes =
|
||||||
|
{
|
||||||
|
new GalaxyAttribute
|
||||||
|
{
|
||||||
|
AttributeName = "Arr",
|
||||||
|
// Galaxy SQL already appends "[]" to array attribute references.
|
||||||
|
FullTagReference = "Obj.Arr[]",
|
||||||
|
IsArray = true,
|
||||||
|
},
|
||||||
|
new GalaxyAttribute
|
||||||
|
{
|
||||||
|
AttributeName = "Scalar",
|
||||||
|
FullTagReference = "Obj.Scalar",
|
||||||
|
IsArray = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
|
||||||
|
{
|
||||||
|
Status = GalaxyCacheStatus.Healthy,
|
||||||
|
Objects = objects,
|
||||||
|
Index = GalaxyHierarchyIndex.Build(objects),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ArrayAddressNormalizer(new StubGalaxyHierarchyCache(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
|
||||||
|
{
|
||||||
|
/// <summary>Gets the current cache entry.</summary>
|
||||||
|
public GalaxyHierarchyCacheEntry Current { get; } = current;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user