Files
FlowScope/My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs
2026-05-20 16:16:08 +08:00

167 lines
5.9 KiB
C#

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<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 Mapping<T>(
fileName,
new DelegateConfigSource(fileName, loadTextAsync),
new JsonConfigParser());
}
public static ConfigMapping Mapping<T>(
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<Type, object>();
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<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 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<CancellationToken, Task<string>> _loadTextAsync;
public DelegateConfigSource(
string fileName,
Func<CancellationToken, Task<string>> loadTextAsync)
{
Name = fileName;
_loadTextAsync = loadTextAsync ?? throw new ArgumentNullException(nameof(loadTextAsync));
}
public string Name { get; }
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
{
return _loadTextAsync(cancellationToken);
}
}
}
}