140 lines
4.6 KiB
C#
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; }
|
|
}
|
|
}
|