namespace StellaOps.Concelier.Plugin.Unified;
using System.Runtime.CompilerServices;
using StellaOps.Plugin;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Capabilities;
using StellaOps.Plugin.Abstractions.Context;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
using UnifiedFeedCapability = StellaOps.Plugin.Abstractions.Capabilities.IFeedCapability;
using LegacyFeedConnector = StellaOps.Plugin.IFeedConnector;
///
/// Adapts an existing IConnectorPlugin to the unified IPlugin and IFeedCapability interfaces.
/// This enables gradual migration of Concelier connectors to the unified plugin architecture.
///
///
/// The adapter bridges the Concelier-specific IConnectorPlugin/IFeedConnector interfaces to
/// the Plugin.Abstractions IFeedCapability interface. The underlying connector operations
/// are delegated to the wrapped connector.
///
public sealed class FeedPluginAdapter : IPlugin, UnifiedFeedCapability
{
private readonly IConnectorPlugin _plugin;
private readonly FeedType _feedType;
private readonly IReadOnlyList _ecosystems;
private IPluginContext? _context;
private IServiceProvider? _serviceProvider;
private LegacyFeedConnector? _connector;
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
///
/// Creates a new adapter for an existing connector plugin.
///
/// The existing connector plugin to wrap.
/// The feed type classification.
/// Ecosystems covered by this feed.
public FeedPluginAdapter(
IConnectorPlugin plugin,
FeedType feedType,
IReadOnlyList ecosystems)
{
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
_feedType = feedType;
_ecosystems = ecosystems ?? Array.Empty();
}
#region IPlugin
///
public PluginInfo Info => new(
Id: $"com.stellaops.feed.{_plugin.Name}",
Name: _plugin.Name,
Version: "1.0.0",
Vendor: "Stella Ops",
Description: $"Vulnerability feed connector for {_plugin.Name}");
///
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
///
public PluginCapabilities Capabilities => PluginCapabilities.Feed | PluginCapabilities.Network;
///
public PluginLifecycleState State => _state;
///
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
{
_context = context;
_state = PluginLifecycleState.Initializing;
try
{
// Get service provider from context
// Note: This would require IPluginContext to expose the service provider
// For now we create a minimal service provider
_serviceProvider = CreateServiceProvider(context);
if (!_plugin.IsAvailable(_serviceProvider))
{
_state = PluginLifecycleState.Failed;
throw new InvalidOperationException(
$"Feed connector '{_plugin.Name}' is not available.");
}
// Create the actual connector
_connector = _plugin.Create(_serviceProvider);
_state = PluginLifecycleState.Active;
context.Logger.Info("Feed connector adapter initialized for {FeedId}", _plugin.Name);
}
catch (Exception ex)
{
_state = PluginLifecycleState.Failed;
context.Logger.Error(ex, "Failed to initialize feed connector {FeedId}", _plugin.Name);
throw;
}
await Task.CompletedTask;
}
///
public async Task HealthCheckAsync(CancellationToken ct)
{
try
{
if (_state != PluginLifecycleState.Active)
{
return HealthCheckResult.Unhealthy($"Connector is in state {_state}");
}
if (_connector == null)
{
return HealthCheckResult.Unhealthy("Connector not initialized");
}
return HealthCheckResult.Healthy()
.WithDetails(new Dictionary
{
["feedId"] = FeedId,
["feedType"] = _feedType.ToString(),
["ecosystems"] = string.Join(", ", _ecosystems),
["sourceName"] = _connector.SourceName
});
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy(ex);
}
}
///
public ValueTask DisposeAsync()
{
if (_connector is IAsyncDisposable asyncDisposable)
{
return asyncDisposable.DisposeAsync();
}
if (_connector is IDisposable disposable)
{
disposable.Dispose();
}
_connector = null;
_state = PluginLifecycleState.Stopped;
return ValueTask.CompletedTask;
}
#endregion
#region IConnectorCapability
///
public string ConnectorType => $"feed.{_plugin.Name}";
///
public string DisplayName => _plugin.Name;
///
public async Task TestConnectionAsync(CancellationToken ct)
{
if (_state != PluginLifecycleState.Active)
{
return ConnectionTestResult.Failed($"Connector is in state {_state}");
}
try
{
// For feed connectors, we can't easily test connection without a full fetch
// Return success if the connector is initialized
return ConnectionTestResult.Succeeded();
}
catch (Exception ex)
{
return ConnectionTestResult.Failed(ex.Message, ex);
}
}
///
public async Task GetConnectionInfoAsync(CancellationToken ct)
{
return new ConnectionInfo(
EndpointUrl: $"feed://{_plugin.Name}",
AuthenticatedAs: null,
ConnectedSince: null,
Metadata: new Dictionary
{
["feedType"] = _feedType.ToString(),
["ecosystems"] = _ecosystems
});
}
#endregion
#region IFeedCapability
///
public string FeedId => _plugin.Name;
///
public FeedType FeedType => _feedType;
///
public IReadOnlyList CoveredEcosystems => _ecosystems;
///
public async Task GetMetadataAsync(CancellationToken ct)
{
EnsureActive();
// The legacy IFeedConnector doesn't have direct metadata access
// Return basic metadata from the plugin
return new FeedMetadata(
FeedId: FeedId,
LastModified: null,
TotalEntries: null,
SchemaVersion: null,
Description: $"Vulnerability feed from {FeedId}");
}
///
public IAsyncEnumerable FetchEntriesAsync(
DateTimeOffset? since,
CancellationToken ct)
{
EnsureActive();
// The legacy IFeedConnector uses FetchAsync/ParseAsync/MapAsync pipeline
// which doesn't directly stream entries. We document this limitation.
return EmptyAsyncEnumerable.ThrowNotSupported(
$"Direct entry streaming is not supported for {FeedId}. " +
"Use the legacy FetchAsync/ParseAsync/MapAsync pipeline via the connector directly.");
}
///
public async Task GetEntryAsync(string id, CancellationToken ct)
{
EnsureActive();
// The legacy connector doesn't support individual entry lookup
throw new NotSupportedException(
$"Individual entry lookup is not supported for {FeedId}. " +
"Use the legacy pipeline for bulk data processing.");
}
///
public async Task GetRawEntryAsync(string id, CancellationToken ct)
{
EnsureActive();
// Not supported by legacy connector
return null;
}
#endregion
#region Extended Operations
///
/// Gets the underlying connector for extended operations.
///
public LegacyFeedConnector? Connector => _connector;
///
/// Executes the fetch phase of the legacy pipeline.
///
public async Task FetchAsync(CancellationToken ct)
{
EnsureActive();
await _connector!.FetchAsync(_serviceProvider!, ct);
}
///
/// Executes the parse phase of the legacy pipeline.
///
public async Task ParseAsync(CancellationToken ct)
{
EnsureActive();
await _connector!.ParseAsync(_serviceProvider!, ct);
}
///
/// Executes the map phase of the legacy pipeline.
///
public async Task MapAsync(CancellationToken ct)
{
EnsureActive();
await _connector!.MapAsync(_serviceProvider!, ct);
}
///
/// Executes the full pipeline (fetch, parse, map).
///
public async Task RunPipelineAsync(CancellationToken ct)
{
await FetchAsync(ct);
await ParseAsync(ct);
await MapAsync(ct);
}
#endregion
#region Helpers
private void EnsureActive()
{
if (_state != PluginLifecycleState.Active || _connector == null)
{
throw new InvalidOperationException(
$"Feed connector '{_plugin.Name}' is not active (state: {_state})");
}
}
private IServiceProvider CreateServiceProvider(IPluginContext context)
{
// The adapter needs to provide services that the legacy connector expects
// For now, return a minimal provider that wraps the context
return new MinimalServiceProvider(context);
}
private sealed class MinimalServiceProvider : IServiceProvider
{
private readonly IPluginContext _context;
public MinimalServiceProvider(IPluginContext context)
{
_context = context;
}
public object? GetService(Type serviceType)
{
if (serviceType == typeof(IPluginContext))
return _context;
if (serviceType == typeof(IPluginLogger))
return _context.Logger;
return null;
}
}
#endregion
}
///
/// Helper for creating async enumerables that throw exceptions.
///
internal static class EmptyAsyncEnumerable
{
public static IAsyncEnumerable ThrowNotSupported(string message)
{
return new ThrowingAsyncEnumerable(message);
}
private sealed class ThrowingAsyncEnumerable : IAsyncEnumerable
{
private readonly string _message;
public ThrowingAsyncEnumerable(string message) => _message = message;
public IAsyncEnumerator GetAsyncEnumerator(CancellationToken ct) =>
new ThrowingAsyncEnumerator(_message);
}
private sealed class ThrowingAsyncEnumerator : IAsyncEnumerator
{
private readonly string _message;
public ThrowingAsyncEnumerator(string message) => _message = message;
public T Current => throw new NotSupportedException(_message);
public ValueTask MoveNextAsync() =>
throw new NotSupportedException(_message);
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}