133 lines
4.7 KiB
C#
133 lines
4.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FlowScope.Data;
|
|
|
|
namespace FlowScope.Config
|
|
{
|
|
public sealed class JsonConfigProvider : IConfigProvider
|
|
{
|
|
private readonly IReadOnlyList<ConfigMapping> _mappings;
|
|
private readonly Dictionary<Type, object> _rowsByType = new();
|
|
|
|
public JsonConfigProvider(IReadOnlyList<ConfigMapping> mappings)
|
|
{
|
|
_mappings = mappings ?? throw new ArgumentNullException(nameof(mappings));
|
|
}
|
|
|
|
public static ConfigMapping Mapping<T>(string fileName, Func<CancellationToken, Task<string>> loadTextAsync)
|
|
where T : class, IConfigRow
|
|
{
|
|
return new ConfigMapping(typeof(T), fileName, loadTextAsync);
|
|
}
|
|
|
|
public async Task LoadAllAsync(CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var loadedRows = new Dictionary<Type, object>();
|
|
|
|
foreach (var mapping in _mappings)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
string json;
|
|
try
|
|
{
|
|
json = await mapping.LoadTextAsync(cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
throw new InvalidOperationException($"Failed to load config file '{mapping.FileName}'.", exception);
|
|
}
|
|
|
|
try
|
|
{
|
|
loadedRows[mapping.RowType] = mapping.Parse(json);
|
|
}
|
|
catch (Exception exception) when (exception is not OperationCanceledException)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Failed to parse config file '{mapping.FileName}': {exception.GetBaseException().Message}",
|
|
exception);
|
|
}
|
|
}
|
|
|
|
_rowsByType.Clear();
|
|
foreach (var pair in loadedRows)
|
|
{
|
|
_rowsByType[pair.Key] = pair.Value;
|
|
}
|
|
}
|
|
|
|
public T Get<T>(int id) where T : class, IConfigRow
|
|
{
|
|
return _rowsByType.TryGetValue(typeof(T), out var table) &&
|
|
table is Dictionary<int, T> rows &&
|
|
rows.TryGetValue(id, out var row)
|
|
? row
|
|
: null;
|
|
}
|
|
|
|
public IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow
|
|
{
|
|
if (!_rowsByType.TryGetValue(typeof(T), out var table) || table is not Dictionary<int, T> rows)
|
|
{
|
|
return Array.Empty<T>();
|
|
}
|
|
|
|
return new List<T>(rows.Values);
|
|
}
|
|
|
|
public sealed class ConfigMapping
|
|
{
|
|
private readonly Func<string, object> _parse;
|
|
|
|
internal ConfigMapping(Type rowType, string fileName, Func<CancellationToken, Task<string>> loadTextAsync)
|
|
{
|
|
RowType = rowType ?? throw new ArgumentNullException(nameof(rowType));
|
|
FileName = string.IsNullOrWhiteSpace(fileName)
|
|
? throw new ArgumentException("Config file name is required.", nameof(fileName))
|
|
: fileName;
|
|
LoadTextAsync = loadTextAsync ?? throw new ArgumentNullException(nameof(loadTextAsync));
|
|
_parse = CreateParser(rowType);
|
|
}
|
|
|
|
public Type RowType { get; }
|
|
public string FileName { get; }
|
|
public Func<CancellationToken, Task<string>> LoadTextAsync { get; }
|
|
|
|
internal object Parse(string json) => _parse(json);
|
|
|
|
private static Func<string, object> CreateParser(Type rowType)
|
|
{
|
|
var method = typeof(ConfigMapping)
|
|
.GetMethod(nameof(ParseRows), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)
|
|
.MakeGenericMethod(rowType);
|
|
|
|
return json => method.Invoke(null, new object[] { json });
|
|
}
|
|
|
|
private static object ParseRows<T>(string json) where T : class, IConfigRow
|
|
{
|
|
var rows = ReactivePropertyJsonConverter.DeserializeArray<T>(json);
|
|
var byId = new Dictionary<int, T>();
|
|
foreach (var row in rows)
|
|
{
|
|
if (byId.ContainsKey(row.Id))
|
|
{
|
|
throw new InvalidOperationException($"Duplicate config id {row.Id} for {typeof(T).Name}.");
|
|
}
|
|
|
|
byId.Add(row.Id, row);
|
|
}
|
|
|
|
return byId;
|
|
}
|
|
}
|
|
}
|
|
}
|