using StellaOps.TaskRunner.Core.Execution; using System.Text.Json; using System.Text.Json.Nodes; namespace StellaOps.TaskRunner.Infrastructure.Execution; public sealed class FilePackRunApprovalStore : IPackRunApprovalStore { private readonly string rootPath; private readonly JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true }; public FilePackRunApprovalStore(string rootPath) { ArgumentException.ThrowIfNullOrWhiteSpace(rootPath); this.rootPath = rootPath; Directory.CreateDirectory(rootPath); } public Task SaveAsync(string runId, IReadOnlyList approvals, CancellationToken cancellationToken) { var path = GetFilePath(runId); var json = SerializeApprovals(approvals); File.WriteAllText(path, json); return Task.CompletedTask; } public Task> GetAsync(string runId, CancellationToken cancellationToken) { var path = GetFilePath(runId); if (!File.Exists(path)) { return Task.FromResult((IReadOnlyList)Array.Empty()); } var json = File.ReadAllText(path); var approvals = DeserializeApprovals(json); return Task.FromResult((IReadOnlyList)approvals); } public async Task UpdateAsync(string runId, PackRunApprovalState approval, CancellationToken cancellationToken) { var approvals = (await GetAsync(runId, cancellationToken).ConfigureAwait(false)).ToList(); var index = approvals.FindIndex(existing => string.Equals(existing.ApprovalId, approval.ApprovalId, StringComparison.Ordinal)); if (index < 0) { throw new InvalidOperationException($"Approval '{approval.ApprovalId}' not found for run '{runId}'."); } approvals[index] = approval; await SaveAsync(runId, approvals, cancellationToken).ConfigureAwait(false); } private string GetFilePath(string runId) { var safeFile = $"{runId}.json"; return Path.Combine(rootPath, safeFile); } private string SerializeApprovals(IReadOnlyList approvals) { var array = new JsonArray(); foreach (var approval in approvals) { var node = new JsonObject { ["approvalId"] = approval.ApprovalId, ["status"] = approval.Status.ToString(), ["requestedAt"] = approval.RequestedAt, ["actorId"] = approval.ActorId, ["completedAt"] = approval.CompletedAt, ["summary"] = approval.Summary, ["requiredGrants"] = new JsonArray(approval.RequiredGrants.Select(grant => (JsonNode)grant).ToArray()), ["stepIds"] = new JsonArray(approval.StepIds.Select(step => (JsonNode)step).ToArray()), ["messages"] = new JsonArray(approval.Messages.Select(message => (JsonNode)message).ToArray()), ["reasonTemplate"] = approval.ReasonTemplate }; array.Add(node); } return array.ToJsonString(serializerOptions); } private static IReadOnlyList DeserializeApprovals(string json) { var array = JsonNode.Parse(json)?.AsArray() ?? new JsonArray(); var list = new List(array.Count); foreach (var entry in array) { if (entry is not JsonObject obj) { continue; } var requiredGrants = obj["requiredGrants"]?.AsArray()?.Select(node => node!.GetValue()).ToList() ?? new List(); var stepIds = obj["stepIds"]?.AsArray()?.Select(node => node!.GetValue()).ToList() ?? new List(); var messages = obj["messages"]?.AsArray()?.Select(node => node!.GetValue()).ToList() ?? new List(); Enum.TryParse(obj["status"]?.GetValue(), ignoreCase: true, out PackRunApprovalStatus status); list.Add(new PackRunApprovalState( obj["approvalId"]?.GetValue() ?? string.Empty, requiredGrants, stepIds, messages, obj["reasonTemplate"]?.GetValue(), obj["requestedAt"]?.GetValue() ?? DateTimeOffset.UtcNow, status, obj["actorId"]?.GetValue(), obj["completedAt"]?.GetValue(), obj["summary"]?.GetValue())); } return list; } }