feat(api): NodeRegistry + ConnectionProviderRegistry (DI-collected)

This commit is contained in:
Claude
2026-08-28 11:58:38 +00:00
parent 77a4ef0c35
commit 857ddd67ea
5 changed files with 121 additions and 0 deletions

View File

@@ -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.

View File

@@ -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}");
}

View 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}");
}

View 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"));
}
}

View 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() }));
}
}