feat(api): NodeRegistry + ConnectionProviderRegistry (DI-collected)
This commit is contained in:
@@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using VolcanicCore.Api;
|
||||
using VolcanicCore.Api.Auth;
|
||||
using VolcanicCore.Api.Endpoints;
|
||||
using VolcanicCore.Api.Registries;
|
||||
using VolcanicCore.Core;
|
||||
using VolcanicCore.Persistence;
|
||||
using VolcanicCore.Persistence.Migrations;
|
||||
using VolcanicCore.Persistence.Repositories;
|
||||
@@ -59,6 +61,14 @@ builder.Services
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Reflection scan — pick up INodeType / IConnectionProvider implementations
|
||||
// registered as DI services by any loaded assembly. In Plan 2, node/provider
|
||||
// assemblies will register their types via extension methods.
|
||||
builder.Services.AddSingleton<NodeRegistry>(sp =>
|
||||
new NodeRegistry(sp.GetServices<INodeType>()));
|
||||
builder.Services.AddSingleton<ConnectionProviderRegistry>(sp =>
|
||||
new ConnectionProviderRegistry(sp.GetServices<IConnectionProvider>()));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply migrations at boot.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using VolcanicCore.Core;
|
||||
|
||||
namespace VolcanicCore.Api.Registries;
|
||||
|
||||
public sealed class ConnectionProviderRegistry
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, IConnectionProvider> _byId;
|
||||
public IReadOnlyList<IConnectionProvider> All { get; }
|
||||
|
||||
public ConnectionProviderRegistry(IEnumerable<IConnectionProvider> providers)
|
||||
{
|
||||
All = providers.ToArray();
|
||||
var byId = new Dictionary<string, IConnectionProvider>(StringComparer.Ordinal);
|
||||
foreach (var p in All)
|
||||
if (!byId.TryAdd(p.Id, p))
|
||||
throw new InvalidOperationException($"Duplicate connection provider id: {p.Id}");
|
||||
_byId = byId;
|
||||
}
|
||||
|
||||
public IConnectionProvider Get(string id) =>
|
||||
_byId.TryGetValue(id, out var p) ? p
|
||||
: throw new KeyNotFoundException($"Unknown connection provider: {id}");
|
||||
}
|
||||
25
src/VolcanicCore.Api/Registries/NodeRegistry.cs
Normal file
25
src/VolcanicCore.Api/Registries/NodeRegistry.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using VolcanicCore.Core;
|
||||
|
||||
namespace VolcanicCore.Api.Registries;
|
||||
|
||||
public sealed class NodeRegistry
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, INodeType> _byId;
|
||||
public IReadOnlyList<INodeType> All { get; }
|
||||
|
||||
public NodeRegistry(IEnumerable<INodeType> nodes)
|
||||
{
|
||||
All = nodes.ToArray();
|
||||
var byId = new Dictionary<string, INodeType>(StringComparer.Ordinal);
|
||||
foreach (var n in All)
|
||||
{
|
||||
if (!byId.TryAdd(n.Id, n))
|
||||
throw new InvalidOperationException($"Duplicate node type id: {n.Id}");
|
||||
}
|
||||
_byId = byId;
|
||||
}
|
||||
|
||||
public INodeType Get(string id) =>
|
||||
_byId.TryGetValue(id, out var n) ? n
|
||||
: throw new KeyNotFoundException($"Unknown node type: {id}");
|
||||
}
|
||||
25
tests/VolcanicCore.Tests/ConnectionProviderRegistryTests.cs
Normal file
25
tests/VolcanicCore.Tests/ConnectionProviderRegistryTests.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json;
|
||||
using VolcanicCore.Api.Registries;
|
||||
using VolcanicCore.Core;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class ConnectionProviderRegistryTests
|
||||
{
|
||||
private sealed class FakeProvider : IConnectionProvider
|
||||
{
|
||||
public string Id => "test.provider";
|
||||
public string DisplayName => "Fake";
|
||||
public JsonSchema ConfigSchema => new(JsonDocument.Parse("{}").RootElement);
|
||||
public Task<object> OpenAsync(JsonElement config, CancellationToken ct) => Task.FromResult<object>(new());
|
||||
public Task CloseAsync(object handle) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registers_and_looks_up_by_id()
|
||||
{
|
||||
var reg = new ConnectionProviderRegistry(new IConnectionProvider[] { new FakeProvider() });
|
||||
Assert.Single(reg.All);
|
||||
Assert.Same(reg.All[0], reg.Get("test.provider"));
|
||||
}
|
||||
}
|
||||
38
tests/VolcanicCore.Tests/NodeRegistryTests.cs
Normal file
38
tests/VolcanicCore.Tests/NodeRegistryTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using VolcanicCore.Api.Registries;
|
||||
using VolcanicCore.Core;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class NodeRegistryTests
|
||||
{
|
||||
private sealed class FakeAction : IActionNode
|
||||
{
|
||||
public string Id => "test.echo";
|
||||
public string DisplayName => "Echo";
|
||||
public NodeCategory Category => NodeCategory.Action;
|
||||
public int InputCount => 1;
|
||||
public int OutputCount => 1;
|
||||
public string? ConnectionProviderId => null;
|
||||
public JsonSchema ConfigSchema => new(JsonDocument.Parse("{}").RootElement);
|
||||
public Task<IReadOnlyList<IReadOnlyList<Item>>> ExecuteAsync(
|
||||
INodeContext ctx, IReadOnlyList<IReadOnlyList<Item>> inputsByPort, CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<IReadOnlyList<Item>>>(new[] { inputsByPort[0] });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registers_instances_and_looks_them_up_by_id()
|
||||
{
|
||||
var reg = new NodeRegistry(new INodeType[] { new FakeAction() });
|
||||
Assert.Single(reg.All);
|
||||
Assert.Same(reg.All[0], reg.Get("test.echo"));
|
||||
Assert.Throws<KeyNotFoundException>(() => reg.Get("does.not.exist"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duplicate_ids_throw()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
new NodeRegistry(new INodeType[] { new FakeAction(), new FakeAction() }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user