using System; using System.Collections.Generic; using System.Linq; using MongoDB.Bson; namespace StellaOps.Concelier.Connector.Distro.Debian.Internal; internal sealed record DebianCursor( DateTimeOffset? LastPublished, IReadOnlyCollection ProcessedAdvisoryIds, IReadOnlyCollection PendingDocuments, IReadOnlyCollection PendingMappings, IReadOnlyDictionary FetchCache) { private static readonly IReadOnlyCollection EmptyIds = Array.Empty(); private static readonly IReadOnlyCollection EmptyGuidList = Array.Empty(); private static readonly IReadOnlyDictionary EmptyCache = new Dictionary(StringComparer.OrdinalIgnoreCase); public static DebianCursor Empty { get; } = new(null, EmptyIds, EmptyGuidList, EmptyGuidList, EmptyCache); public static DebianCursor FromBson(BsonDocument? document) { if (document is null || document.ElementCount == 0) { return Empty; } DateTimeOffset? lastPublished = null; if (document.TryGetValue("lastPublished", out var lastValue)) { lastPublished = lastValue.BsonType switch { BsonType.String when DateTimeOffset.TryParse(lastValue.AsString, out var parsed) => parsed.ToUniversalTime(), BsonType.DateTime => DateTime.SpecifyKind(lastValue.ToUniversalTime(), DateTimeKind.Utc), _ => null, }; } var processed = ReadStringArray(document, "processedIds"); var pendingDocuments = ReadGuidArray(document, "pendingDocuments"); var pendingMappings = ReadGuidArray(document, "pendingMappings"); var cache = ReadCache(document); return new DebianCursor(lastPublished, processed, pendingDocuments, pendingMappings, cache); } public BsonDocument ToBsonDocument() { var document = new BsonDocument { ["pendingDocuments"] = new BsonArray(PendingDocuments.Select(static id => id.ToString())), ["pendingMappings"] = new BsonArray(PendingMappings.Select(static id => id.ToString())), }; if (LastPublished.HasValue) { document["lastPublished"] = LastPublished.Value.UtcDateTime; } if (ProcessedAdvisoryIds.Count > 0) { document["processedIds"] = new BsonArray(ProcessedAdvisoryIds); } if (FetchCache.Count > 0) { var cacheDoc = new BsonDocument(); foreach (var (key, entry) in FetchCache) { cacheDoc[key] = entry.ToBsonDocument(); } document["fetchCache"] = cacheDoc; } return document; } public DebianCursor WithPendingDocuments(IEnumerable ids) => this with { PendingDocuments = ids?.Distinct().ToArray() ?? EmptyGuidList }; public DebianCursor WithPendingMappings(IEnumerable ids) => this with { PendingMappings = ids?.Distinct().ToArray() ?? EmptyGuidList }; public DebianCursor WithProcessed(DateTimeOffset published, IEnumerable ids) => this with { LastPublished = published.ToUniversalTime(), ProcessedAdvisoryIds = ids?.Where(static id => !string.IsNullOrWhiteSpace(id)) .Select(static id => id.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? EmptyIds }; public DebianCursor WithFetchCache(IDictionary? cache) { if (cache is null || cache.Count == 0) { return this with { FetchCache = EmptyCache }; } return this with { FetchCache = new Dictionary(cache, StringComparer.OrdinalIgnoreCase) }; } public bool TryGetCache(string key, out DebianFetchCacheEntry entry) { if (FetchCache.Count == 0) { entry = DebianFetchCacheEntry.Empty; return false; } return FetchCache.TryGetValue(key, out entry!); } private static IReadOnlyCollection ReadStringArray(BsonDocument document, string field) { if (!document.TryGetValue(field, out var value) || value is not BsonArray array) { return EmptyIds; } var list = new List(array.Count); foreach (var element in array) { if (element.BsonType == BsonType.String) { var str = element.AsString.Trim(); if (!string.IsNullOrEmpty(str)) { list.Add(str); } } } return list; } private static IReadOnlyCollection ReadGuidArray(BsonDocument document, string field) { if (!document.TryGetValue(field, out var value) || value is not BsonArray array) { return EmptyGuidList; } var list = new List(array.Count); foreach (var element in array) { if (Guid.TryParse(element.ToString(), out var guid)) { list.Add(guid); } } return list; } private static IReadOnlyDictionary ReadCache(BsonDocument document) { if (!document.TryGetValue("fetchCache", out var value) || value is not BsonDocument cacheDocument || cacheDocument.ElementCount == 0) { return EmptyCache; } var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var element in cacheDocument.Elements) { if (element.Value is BsonDocument entry) { cache[element.Name] = DebianFetchCacheEntry.FromBson(entry); } } return cache; } }