using System.Text.Json; namespace SVSim.Bootstrap.Importers; /// /// Shared helpers for content importers. Loads a prod-capture JSON file by endpoint name from /// a captures directory, returning the inner data element. Picks the latest matching dated /// file (e.g. load-index-2026-05-23.json) if multiple exist for the same endpoint. /// public static class ImporterBase { /// /// Returns the parsed .data JsonElement for the latest {endpoint}-*.json file in /// , or null if no file matches. Logs a warning when missing — /// caller decides whether absence is fatal. /// public static JsonElement? LoadCapture(string capturesDir, string endpoint) { if (!Directory.Exists(capturesDir)) { Console.Error.WriteLine($"[ImporterBase] Captures dir missing: {capturesDir}"); return null; } string? path = Directory.EnumerateFiles(capturesDir, $"{endpoint}-*.json") .OrderByDescending(p => p) .FirstOrDefault(); if (path is null) { Console.Error.WriteLine($"[ImporterBase] No capture found for endpoint '{endpoint}' in {capturesDir}"); return null; } using var fs = File.OpenRead(path); using var doc = JsonDocument.Parse(fs); if (!doc.RootElement.TryGetProperty("data", out var data)) { Console.Error.WriteLine($"[ImporterBase] Capture file {path} has no top-level 'data' property."); return null; } // Clone so the JsonElement survives doc disposal. return data.Clone(); } /// /// Generic upsert by primary key. Returns (created, updated, unchanged) counts. /// is the desired state from the capture; rows are matched by /// . mutates an existing row to /// reflect incoming values and returns true if anything actually changed. /// public static (int created, int updated, int unchanged) Upsert( IEnumerable incoming, Dictionary existingByKey, Func keySelector, Action addToContext, Func applyChanges) where TKey : notnull { int created = 0, updated = 0, unchanged = 0; foreach (var item in incoming) { var key = keySelector(item); if (existingByKey.TryGetValue(key, out var existing)) { if (applyChanges(existing, item)) updated++; else unchanged++; } else { addToContext(item); existingByKey[key] = item; created++; } } return (created, updated, unchanged); } /// Serialize a JsonElement back to compact JSON text for jsonb storage. public static string Serialize(JsonElement el) => JsonSerializer.Serialize(el, new JsonSerializerOptions { WriteIndented = false }); /// Parse a wire date that may be ISO ("2026-05-23T..."), space-separated ("2026-05-23 16:32:31"), or empty. public static DateTime ParseWireDateTime(string? s) { if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue; if (DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, out var dt)) { return dt; } return DateTime.MinValue; } /// Read a JsonElement string/number property as long, defaulting on missing/null. public static long GetLong(JsonElement el, string prop, long fallback = 0) { if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; if (v.ValueKind == JsonValueKind.Number) return v.GetInt64(); if (v.ValueKind == JsonValueKind.String && long.TryParse(v.GetString(), out var n)) return n; return fallback; } public static int GetInt(JsonElement el, string prop, int fallback = 0) { if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; if (v.ValueKind == JsonValueKind.Number) return v.GetInt32(); if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), out var n)) return n; return fallback; } public static string GetString(JsonElement el, string prop, string fallback = "") { if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; return v.ValueKind == JsonValueKind.String ? v.GetString() ?? fallback : v.ToString(); } public static bool GetBool(JsonElement el, string prop, bool fallback = false) { if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; if (v.ValueKind == JsonValueKind.True) return true; if (v.ValueKind == JsonValueKind.False) return false; if (v.ValueKind == JsonValueKind.Number) return v.GetInt32() != 0; if (v.ValueKind == JsonValueKind.String) { var s = v.GetString(); if (bool.TryParse(s, out var b)) return b; if (int.TryParse(s, out var i)) return i != 0; } return fallback; } public static ulong GetULong(JsonElement el, string prop, ulong fallback = 0) { if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback; if (v.ValueKind == JsonValueKind.Number) return v.GetUInt64(); if (v.ValueKind == JsonValueKind.String && ulong.TryParse(v.GetString(), out var n)) return n; return fallback; } }