using Microsoft.Extensions.Hosting; using System.Text; using System.Text.Json; namespace StellaOps.Policy.Engine.Overlay; /// /// Persists overlay projections as NDJSON files under overlay/{tenant}/{ruleId}/{version}.ndjson. /// internal sealed class FileOverlayStore : IOverlayStore { private readonly string _root; public FileOverlayStore(IHostEnvironment env) { _root = Path.Combine(env.ContentRootPath, "overlay"); } public async Task SaveAsync(OverlayProjection projection, CancellationToken cancellationToken = default) { if (projection is null) { throw new ArgumentNullException(nameof(projection)); } var tenant = Sanitize(projection.Tenant); var rule = Sanitize(projection.RuleId); var dir = Path.Combine(_root, tenant, rule); Directory.CreateDirectory(dir); var path = Path.Combine(dir, $"{projection.Version}.ndjson"); var lines = new[] { JsonSerializer.Serialize(new OverlayProjectionHeader("overlay-projection-v1")), JsonSerializer.Serialize(projection) }; await File.WriteAllLinesAsync(path, lines, Encoding.UTF8, cancellationToken).ConfigureAwait(false); } private static string Sanitize(string value) { foreach (var ch in Path.GetInvalidFileNameChars()) { value = value.Replace(ch, '_'); } return string.IsNullOrWhiteSpace(value) ? "unknown" : value; } }