新增 FlowScope Git 包雏形
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class FileConfigSource : IConfigSource
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
|
||||
public FileConfigSource(string rootPath, string name = "File")
|
||||
{
|
||||
_rootPath = string.IsNullOrWhiteSpace(rootPath)
|
||||
? throw new ArgumentException("Config root path is required.", nameof(rootPath))
|
||||
: rootPath;
|
||||
Name = string.IsNullOrWhiteSpace(name)
|
||||
? throw new ArgumentException("Config source name is required.", nameof(name))
|
||||
: name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var path = Path.Combine(_rootPath, fileName);
|
||||
return Task.Run(() => File.ReadAllText(path), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d50cd860ba849a1aa7647deba4e71db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigParser
|
||||
{
|
||||
string Format { get; }
|
||||
|
||||
IReadOnlyList<T> ParseRows<T>(
|
||||
string fileName,
|
||||
string text)
|
||||
where T : class, IConfigRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d22229e25964ea29b3c6d153507b1db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigProvider
|
||||
{
|
||||
Task LoadAllAsync(CancellationToken cancellationToken);
|
||||
T Get<T>(int id) where T : class, IConfigRow;
|
||||
IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af3d9a24ab0e4c6ebe3bf906cfd9e2bb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigRow
|
||||
{
|
||||
int Id { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20d7e0e8230e4df1b5afa8000ef26fea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public interface IConfigSource
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
Task<string> LoadTextAsync(
|
||||
string fileName,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 708db2a1c26743f3994b5d8beb1a03f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class InMemoryConfigSource : IConfigSource
|
||||
{
|
||||
private readonly Dictionary<string, string> _files;
|
||||
|
||||
public InMemoryConfigSource(
|
||||
IReadOnlyDictionary<string, string> files,
|
||||
string name = "InMemory")
|
||||
{
|
||||
_files = files != null
|
||||
? new Dictionary<string, string>(files)
|
||||
: throw new ArgumentNullException(nameof(files));
|
||||
Name = string.IsNullOrWhiteSpace(name)
|
||||
? throw new ArgumentException("Config source name is required.", nameof(name))
|
||||
: name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!_files.TryGetValue(fileName, out var text))
|
||||
{
|
||||
throw new InvalidOperationException($"Config file is missing: {fileName}");
|
||||
}
|
||||
|
||||
return Task.FromResult(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 607d9b20e7f04565929ea5876e5c25e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using FlowScope.Data;
|
||||
|
||||
namespace FlowScope.Config
|
||||
{
|
||||
public sealed class JsonConfigParser : IConfigParser
|
||||
{
|
||||
public string Format => "json";
|
||||
|
||||
public IReadOnlyList<T> ParseRows<T>(string fileName, string text)
|
||||
where T : class, IConfigRow
|
||||
{
|
||||
return ReactivePropertyJsonConverter.DeserializeArray<T>(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f60f8a8e66394f26a20ed6f5ef314c63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b6c05d7a1d74798901d3efcc51cb7c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user