refactor(bootstrap): finalize seed migration; remove GlobalsImporter and prod-captures plumbing

Final cleanup of the bootstrap-seed refactor (Task 10 of 10):

- Delete the GlobalsImporter no-op stub and its two remaining call sites
  (Program.cs and SVSimTestFactory.cs). All work has moved to per-domain
  importers since Task 9.
- Drop the --captures CLI flag and CapturesDir / shippedCaptures plumbing
  from Program.cs (BootstrapOptions, ParseArgs, PrintUsage). Bootstrap input
  is now cards.json + reference CSVs + per-table seed JSON; no more
  prod-captures directory.
- Shrink ImporterBase from 141 to 23 lines: LoadCapture, Serialize,
  Upsert<T,TKey>, GetInt/GetString/GetBool/GetLong/GetULong all had zero
  callers after the seed migration. Only ParseWireDateTime survives (still
  used by PaymentItemImporter and MyPageGlobalsImporter for prod-shaped
  timestamp strings).
- Remove the prod-captures Content Include glob from SVSim.Bootstrap.csproj
  and both prod-captures globs (production + test-fixture overlay) from
  SVSim.UnitTests.csproj. Test fixtures now overlay production seeds at
  Data/seeds/ via the Task 7 layout exclusively.

Build clean; 391/391 unit tests passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-26 16:06:46 -04:00
parent d14a0be2c8
commit c02991a5c2
6 changed files with 26 additions and 186 deletions

View File

@@ -1,84 +1,13 @@
using System.Text.Json;
namespace SVSim.Bootstrap.Importers;
/// <summary>
/// Shared helpers for content importers. Loads a prod-capture JSON file by endpoint name from
/// a captures directory, returning the inner <c>data</c> element. Picks the latest matching dated
/// file (e.g. <c>load-index-2026-05-23.json</c>) if multiple exist for the same endpoint.
/// Tiny shared helper for content importers. Capture parsing has moved out of the bootstrap
/// project entirely (extractors under <c>data_dumps/extract/</c> emit per-table seed JSON);
/// only the wire-date normaliser stays here because several seed-driven importers still need
/// to canonicalise prod-shaped timestamp strings.
/// </summary>
public static class ImporterBase
{
/// <summary>
/// Returns the parsed <c>.data</c> JsonElement for the latest <c>{endpoint}-*.json</c> file in
/// <paramref name="capturesDir"/>, or null if no file matches. Logs a warning when missing —
/// caller decides whether absence is fatal.
/// </summary>
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();
}
/// <summary>
/// Generic upsert by primary key. Returns (created, updated, unchanged) counts.
/// <paramref name="incoming"/> is the desired state from the capture; rows are matched by
/// <paramref name="keySelector"/>. <paramref name="applyChanges"/> mutates an existing row to
/// reflect incoming values and returns true if anything actually changed.
/// </summary>
public static (int created, int updated, int unchanged) Upsert<T, TKey>(
IEnumerable<T> incoming,
Dictionary<TKey, T> existingByKey,
Func<T, TKey> keySelector,
Action<T> addToContext,
Func<T, T, bool> 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);
}
/// <summary>Serialize a JsonElement back to compact JSON text for jsonb storage.</summary>
public static string Serialize(JsonElement el) =>
JsonSerializer.Serialize(el, new JsonSerializerOptions { WriteIndented = false });
/// <summary>Parse a wire date that may be ISO ("2026-05-23T..."), space-separated ("2026-05-23 16:32:31"), or empty.</summary>
public static DateTime ParseWireDateTime(string? s)
{
@@ -91,50 +20,4 @@ public static class ImporterBase
}
return DateTime.MinValue;
}
/// <summary>Read a JsonElement string/number property as long, defaulting on missing/null.</summary>
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;
}
}