Files
voltsrage 80b009fd23
CI / backend (push) Successful in 8m52s
CI / frontend (push) Failing after 1m39s
feature: Self-Service Clinical Testing Sessions
2026-08-06 04:03:04 +08:00

140 lines
4.6 KiB
C#

using System.Text.Json;
using Microsoft.Extensions.Options;
public interface ISessionCatalog
{
IReadOnlyList<SessionPreset> ListSessions();
SessionPreset? GetById(string sessionId);
}
public sealed record SessionPreset(
string Id,
string Name,
string Goal,
int EstimatedMinutes,
double DefaultSpeed,
IReadOnlyList<string> Scenarios);
/// <summary>
/// Loads <c>sessions.json</c> from the scenario directory (or its parent) and
/// validates every referenced scenario id against <see cref="IScenarioCatalog"/>
/// at construction — a typo fails loudly at startup.
/// </summary>
public sealed class SessionCatalog : ISessionCatalog
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
private readonly IReadOnlyList<SessionPreset> _sessions;
public SessionCatalog(IOptions<SimulationOptions> options, IScenarioCatalog scenarios)
{
var directory = options.Value.ScenarioDirectory;
var path = ResolveManifestPath(directory)
?? throw new InvalidOperationException(
$"Simulation session manifest not found. Expected sessions.json in or beside " +
$"ScenarioDirectory '{Path.GetFullPath(directory)}'.");
_sessions = LoadAndValidate(path, scenarios);
}
public IReadOnlyList<SessionPreset> ListSessions() => _sessions;
public SessionPreset? GetById(string sessionId) =>
_sessions.FirstOrDefault(s =>
string.Equals(s.Id, sessionId, StringComparison.OrdinalIgnoreCase));
internal static string? ResolveManifestPath(string scenarioDirectory)
{
var inDir = Path.Combine(scenarioDirectory, "sessions.json");
if (File.Exists(inDir))
return inDir;
var parent = Directory.GetParent(Path.GetFullPath(scenarioDirectory))?.FullName;
if (parent is not null)
{
var beside = Path.Combine(parent, "sessions.json");
if (File.Exists(beside))
return beside;
}
return null;
}
internal static IReadOnlyList<SessionPreset> LoadAndValidate(
string path, IScenarioCatalog scenarios)
{
SessionsDocument doc;
try
{
var json = File.ReadAllText(path);
doc = JsonSerializer.Deserialize<SessionsDocument>(json, JsonOptions)
?? throw new InvalidOperationException(
$"Session manifest '{path}' deserialized to null.");
}
catch (Exception ex) when (ex is not InvalidOperationException)
{
throw new InvalidOperationException(
$"Failed to load simulation session manifest '{path}': {ex.Message}", ex);
}
if (doc.Sessions is null || doc.Sessions.Count == 0)
throw new InvalidOperationException(
$"Session manifest '{path}' contains no sessions.");
var presets = new List<SessionPreset>(doc.Sessions.Count);
var missing = new List<string>();
foreach (var entry in doc.Sessions)
{
if (string.IsNullOrWhiteSpace(entry.Id))
throw new InvalidOperationException(
$"Session manifest '{path}' has a session with an empty id.");
if (entry.Scenarios is null || entry.Scenarios.Count == 0)
throw new InvalidOperationException(
$"Session '{entry.Id}' in '{path}' lists no scenarios.");
foreach (var scenarioId in entry.Scenarios)
{
if (scenarios.GetById(scenarioId) is null)
missing.Add($"{entry.Id} → {scenarioId}");
}
presets.Add(new SessionPreset(
entry.Id,
entry.Name ?? entry.Id,
entry.Goal ?? string.Empty,
entry.EstimatedMinutes,
entry.DefaultSpeed <= 0 ? 60 : entry.DefaultSpeed,
entry.Scenarios.AsReadOnly()));
}
if (missing.Count > 0)
{
throw new InvalidOperationException(
"Session manifest references unknown scenario id(s): " +
string.Join("; ", missing));
}
return presets;
}
private sealed class SessionsDocument
{
public List<SessionEntry>? Sessions { get; set; }
}
private sealed class SessionEntry
{
public string Id { get; set; } = null!;
public string? Name { get; set; }
public string? Goal { get; set; }
public int EstimatedMinutes { get; set; }
public double DefaultSpeed { get; set; }
public List<string>? Scenarios { get; set; }
}
}