Introduce Vexer platform scaffolding and enrich Concelier merge
This commit is contained in:
128
src/StellaOps.Vexer.Export/ExportEngine.cs
Normal file
128
src/StellaOps.Vexer.Export/ExportEngine.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Vexer.Core;
|
||||
using StellaOps.Vexer.Policy;
|
||||
using StellaOps.Vexer.Storage.Mongo;
|
||||
|
||||
namespace StellaOps.Vexer.Export;
|
||||
|
||||
public interface IExportEngine
|
||||
{
|
||||
ValueTask<VexExportManifest> ExportAsync(VexExportRequestContext context, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record VexExportRequestContext(
|
||||
VexQuery Query,
|
||||
VexExportFormat Format,
|
||||
DateTimeOffset RequestedAt,
|
||||
bool ForceRefresh = false);
|
||||
|
||||
public interface IVexExportDataSource
|
||||
{
|
||||
ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record VexExportDataSet(
|
||||
ImmutableArray<VexConsensus> Consensus,
|
||||
ImmutableArray<VexClaim> Claims,
|
||||
ImmutableArray<string> SourceProviders);
|
||||
|
||||
public sealed class VexExportEngine : IExportEngine
|
||||
{
|
||||
private readonly IVexExportStore _exportStore;
|
||||
private readonly IVexPolicyEvaluator _policyEvaluator;
|
||||
private readonly IVexExportDataSource _dataSource;
|
||||
private readonly IReadOnlyDictionary<VexExportFormat, IVexExporter> _exporters;
|
||||
private readonly ILogger<VexExportEngine> _logger;
|
||||
|
||||
public VexExportEngine(
|
||||
IVexExportStore exportStore,
|
||||
IVexPolicyEvaluator policyEvaluator,
|
||||
IVexExportDataSource dataSource,
|
||||
IEnumerable<IVexExporter> exporters,
|
||||
ILogger<VexExportEngine> logger)
|
||||
{
|
||||
_exportStore = exportStore ?? throw new ArgumentNullException(nameof(exportStore));
|
||||
_policyEvaluator = policyEvaluator ?? throw new ArgumentNullException(nameof(policyEvaluator));
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
if (exporters is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(exporters));
|
||||
}
|
||||
|
||||
_exporters = exporters.ToDictionary(x => x.Format);
|
||||
}
|
||||
|
||||
public async ValueTask<VexExportManifest> ExportAsync(VexExportRequestContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
var signature = VexQuerySignature.FromQuery(context.Query);
|
||||
|
||||
if (!context.ForceRefresh)
|
||||
{
|
||||
var cached = await _exportStore.FindAsync(signature, context.Format, cancellationToken).ConfigureAwait(false);
|
||||
if (cached is not null)
|
||||
{
|
||||
_logger.LogInformation("Reusing cached export for {Signature} ({Format})", signature.Value, context.Format);
|
||||
return cached with { FromCache = true };
|
||||
}
|
||||
}
|
||||
|
||||
var dataset = await _dataSource.FetchAsync(context.Query, cancellationToken).ConfigureAwait(false);
|
||||
var exporter = ResolveExporter(context.Format);
|
||||
|
||||
var exportRequest = new VexExportRequest(
|
||||
context.Query,
|
||||
dataset.Consensus,
|
||||
dataset.Claims,
|
||||
context.RequestedAt);
|
||||
|
||||
var digest = exporter.Digest(exportRequest);
|
||||
|
||||
await using var buffer = new MemoryStream();
|
||||
var result = await exporter.SerializeAsync(exportRequest, buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var exportId = FormattableString.Invariant($"exports/{context.RequestedAt:yyyyMMddTHHmmssfffZ}/{digest.Digest}");
|
||||
var manifest = new VexExportManifest(
|
||||
exportId,
|
||||
signature,
|
||||
context.Format,
|
||||
context.RequestedAt,
|
||||
digest,
|
||||
dataset.Claims.Length,
|
||||
dataset.SourceProviders,
|
||||
fromCache: false,
|
||||
consensusRevision: _policyEvaluator.Version,
|
||||
attestation: null,
|
||||
sizeBytes: result.BytesWritten);
|
||||
|
||||
await _exportStore.SaveAsync(manifest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Export generated for {Signature} ({Format}) size={SizeBytes} bytes",
|
||||
signature.Value,
|
||||
context.Format,
|
||||
result.BytesWritten);
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private IVexExporter ResolveExporter(VexExportFormat format)
|
||||
=> _exporters.TryGetValue(format, out var exporter)
|
||||
? exporter
|
||||
: throw new InvalidOperationException($"No exporter registered for format '{format}'.");
|
||||
}
|
||||
|
||||
public static class VexExportServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddVexExportEngine(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IExportEngine, VexExportEngine>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user