using System; using System.Collections.Generic; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace FlowScope.Config { public sealed class JsonConfigProvider : IConfigProvider { private readonly IReadOnlyList _mappings; private readonly Dictionary _rowsByType = new(); public JsonConfigProvider(IReadOnlyList mappings) { _mappings = mappings ?? throw new ArgumentNullException(nameof(mappings)); } public static ConfigMapping Mapping(string fileName, Func> loadTextAsync) where T : class, IConfigRow { return Mapping( fileName, new DelegateConfigSource(fileName, loadTextAsync), new JsonConfigParser()); } public static ConfigMapping Mapping( string fileName, IConfigSource source, IConfigParser parser) where T : class, IConfigRow { return new ConfigMapping(typeof(T), fileName, source, parser); } public async Task LoadAllAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var loadedRows = new Dictionary(); foreach (var mapping in _mappings) { cancellationToken.ThrowIfCancellationRequested(); string json; try { json = await mapping.Source.LoadTextAsync(mapping.FileName, cancellationToken); } catch (OperationCanceledException) { throw; } catch (Exception exception) { throw new InvalidOperationException( $"Failed to load config file '{mapping.FileName}' from source '{mapping.Source.Name}'.", 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(int id) where T : class, IConfigRow { return _rowsByType.TryGetValue(typeof(T), out var table) && table is Dictionary rows && rows.TryGetValue(id, out var row) ? row : null; } public IReadOnlyList GetAll() where T : class, IConfigRow { if (!_rowsByType.TryGetValue(typeof(T), out var table) || table is not Dictionary rows) { return Array.Empty(); } return new List(rows.Values); } public sealed class ConfigMapping { private readonly MethodInfo _parseRowsMethod; internal ConfigMapping( Type rowType, string fileName, IConfigSource source, IConfigParser parser) { RowType = rowType ?? throw new ArgumentNullException(nameof(rowType)); FileName = string.IsNullOrWhiteSpace(fileName) ? throw new ArgumentException("Config file name is required.", nameof(fileName)) : fileName; Source = source ?? throw new ArgumentNullException(nameof(source)); Parser = parser ?? throw new ArgumentNullException(nameof(parser)); _parseRowsMethod = typeof(IConfigParser) .GetMethod(nameof(IConfigParser.ParseRows)) .MakeGenericMethod(rowType); } public Type RowType { get; } public string FileName { get; } public IConfigSource Source { get; } public IConfigParser Parser { get; } internal object Parse(string json) { var rows = (System.Collections.IEnumerable)_parseRowsMethod.Invoke( Parser, new object[] { FileName, json }); var byIdType = typeof(Dictionary<,>).MakeGenericType(typeof(int), RowType); var byId = (System.Collections.IDictionary)Activator.CreateInstance(byIdType); foreach (IConfigRow row in rows) { if (byId.Contains(row.Id)) { throw new InvalidOperationException($"Duplicate config id {row.Id} for {RowType.Name}."); } byId.Add(row.Id, row); } return byId; } } private sealed class DelegateConfigSource : IConfigSource { private readonly Func> _loadTextAsync; public DelegateConfigSource( string fileName, Func> loadTextAsync) { Name = fileName; _loadTextAsync = loadTextAsync ?? throw new ArgumentNullException(nameof(loadTextAsync)); } public string Name { get; } public Task LoadTextAsync(string fileName, CancellationToken cancellationToken) { return _loadTextAsync(cancellationToken); } } } }