Implement Galaxy filters and API key constraints

This commit is contained in:
Joseph Doherty
2026-04-29 13:37:00 -04:00
parent ac2787f619
commit b995c174eb
54 changed files with 4889 additions and 203 deletions
@@ -160,6 +160,37 @@ public sealed class GalaxyRepositoryClientTests
Assert.Contains("repeated page token", exception.Message, StringComparison.Ordinal);
}
[Fact]
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
{
FakeGalaxyRepositoryTransport transport = CreateTransport();
await using GalaxyRepositoryClient client = CreateClient(transport);
await client.DiscoverHierarchyAsync(new DiscoverHierarchyOptions
{
RootContainedPath = "Area1/Line3",
MaxDepth = 2,
CategoryIds = [10, 13],
TemplateChainContains = ["Pump"],
TagNameGlob = "Pump_*",
IncludeAttributes = false,
AlarmBearingOnly = true,
HistorizedOnly = true,
});
DiscoverHierarchyRequest request = Assert.Single(transport.DiscoverHierarchyCalls).Request;
Assert.Equal(DiscoverHierarchyRequest.RootOneofCase.RootContainedPath, request.RootCase);
Assert.Equal("Area1/Line3", request.RootContainedPath);
Assert.Equal(2, request.MaxDepth);
Assert.Equal([10, 13], request.CategoryIds);
Assert.Equal(["Pump"], request.TemplateChainContains);
Assert.Equal("Pump_*", request.TagNameGlob);
Assert.True(request.HasIncludeAttributes);
Assert.False(request.IncludeAttributes);
Assert.True(request.AlarmBearingOnly);
Assert.True(request.HistorizedOnly);
}
[Fact]
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
{
@@ -0,0 +1,24 @@
namespace MxGateway.Client;
public sealed record DiscoverHierarchyOptions
{
public int? RootGobjectId { get; init; }
public string? RootTagName { get; init; }
public string? RootContainedPath { get; init; }
public int? MaxDepth { get; init; }
public IReadOnlyList<int> CategoryIds { get; init; } = Array.Empty<int>();
public IReadOnlyList<string> TemplateChainContains { get; init; } = Array.Empty<string>();
public string? TagNameGlob { get; init; }
public bool? IncludeAttributes { get; init; }
public bool AlarmBearingOnly { get; init; }
public bool HistorizedOnly { get; init; }
}
@@ -144,18 +144,24 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// they may subscribe to via the MxAccessGateway service.
/// </summary>
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
{
return await DiscoverHierarchyAsync(new DiscoverHierarchyOptions(), cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(
DiscoverHierarchyOptions options,
CancellationToken cancellationToken = default)
{
List<GalaxyObject> objects = [];
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
string pageToken = string.Empty;
do
{
DiscoverHierarchyRequest request = CreateDiscoverHierarchyRequest(options);
request.PageSize = DiscoverHierarchyPageSize;
request.PageToken = pageToken;
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
new DiscoverHierarchyRequest
{
PageSize = DiscoverHierarchyPageSize,
PageToken = pageToken,
},
request,
cancellationToken)
.ConfigureAwait(false);
@@ -173,6 +179,49 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
return objects;
}
private static DiscoverHierarchyRequest CreateDiscoverHierarchyRequest(DiscoverHierarchyOptions options)
{
ArgumentNullException.ThrowIfNull(options);
DiscoverHierarchyRequest request = new()
{
AlarmBearingOnly = options.AlarmBearingOnly,
HistorizedOnly = options.HistorizedOnly,
};
if (options.RootGobjectId.HasValue)
{
request.RootGobjectId = options.RootGobjectId.Value;
}
else if (!string.IsNullOrWhiteSpace(options.RootTagName))
{
request.RootTagName = options.RootTagName;
}
else if (!string.IsNullOrWhiteSpace(options.RootContainedPath))
{
request.RootContainedPath = options.RootContainedPath;
}
if (options.MaxDepth.HasValue)
{
request.MaxDepth = options.MaxDepth.Value;
}
request.CategoryIds.Add(options.CategoryIds);
request.TemplateChainContains.Add(options.TemplateChainContains);
if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
{
request.TagNameGlob = options.TagNameGlob;
}
if (options.IncludeAttributes.HasValue)
{
request.IncludeAttributes = options.IncludeAttributes.Value;
}
return request;
}
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
DiscoverHierarchyRequest request,
CancellationToken cancellationToken = default)
+13
View File
@@ -164,6 +164,19 @@ foreach (GalaxyObject galaxyObject in objects)
}
```
Use `DiscoverHierarchyOptions` to request a server-side slice without pulling
the full Galaxy:
```csharp
IReadOnlyList<GalaxyObject> pumps = await repository.DiscoverHierarchyAsync(
new DiscoverHierarchyOptions
{
RootContainedPath = "Area1/Line3",
TagNameGlob = "Pump_*",
IncludeAttributes = false,
});
```
The CLI exposes the same operations:
```powershell