新增 FlowScope Git 包雏形

This commit is contained in:
JSD\13999
2026-06-04 14:54:49 +08:00
parent 37e6d4ca57
commit f5887da9fb
127 changed files with 5330 additions and 0 deletions

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d50cd860ba849a1aa7647deba4e71db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d22229e25964ea29b3c6d153507b1db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af3d9a24ab0e4c6ebe3bf906cfd9e2bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
namespace FlowScope.Config
{
public interface IConfigRow
{
int Id { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20d7e0e8230e4df1b5afa8000ef26fea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 708db2a1c26743f3994b5d8beb1a03f8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 607d9b20e7f04565929ea5876e5c25e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f60f8a8e66394f26a20ed6f5ef314c63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b6c05d7a1d74798901d3efcc51cb7c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: