mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #301 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
This commit is contained in:
commit
4557dde631
@ -52,7 +52,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
var request = new TestValidatedRequest { Value = -1 }; // 无效值
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -62,7 +62,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
TestRetryBehavior.AttemptCount = 0;
|
||||
var request = new TestRetryRequest { ShouldFailTimes = 0 }; // 不失败
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Success"));
|
||||
Assert.That(TestRetryBehavior.AttemptCount, Is.EqualTo(1));
|
||||
@ -82,7 +82,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
tasks.Add(_context!.SendRequestAsync(request).AsTask());
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
|
||||
// 验证所有请求都成功处理
|
||||
@ -102,7 +102,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
for (int i = 0; i < requestCount; i++)
|
||||
{
|
||||
var request = new TestMemoryRequest { Data = new string('x', 1000) };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
// 定期强制GC来测试内存泄漏
|
||||
if (i % 100 == 0)
|
||||
@ -126,7 +126,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
TestTransientErrorHandler.ErrorCount = 0;
|
||||
var request = new TestTransientErrorRequest { MaxErrors = 0 }; // 不出错
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Success"));
|
||||
Assert.That(TestTransientErrorHandler.ErrorCount, Is.EqualTo(0));
|
||||
@ -140,7 +140,8 @@ public class MediatorAdvancedFeaturesTests
|
||||
{
|
||||
try
|
||||
{
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true });
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = true })
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -151,7 +152,8 @@ public class MediatorAdvancedFeaturesTests
|
||||
// 验证断路器已打开,后续请求应该快速失败
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false }));
|
||||
await _context!.SendRequestAsync(new TestCircuitBreakerRequest { ShouldFail = false })
|
||||
.ConfigureAwait(false));
|
||||
stopwatch.Stop();
|
||||
|
||||
// 验证快速失败(应该在很短时间内完成)
|
||||
@ -172,7 +174,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
// 执行saga
|
||||
foreach (var request in requests)
|
||||
{
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 验证所有步骤都成功执行
|
||||
@ -192,10 +194,10 @@ public class MediatorAdvancedFeaturesTests
|
||||
};
|
||||
|
||||
// 执行saga,第二步会失败
|
||||
await _context!.SendRequestAsync(requests[0]);
|
||||
await _context!.SendRequestAsync(requests[0]).ConfigureAwait(false);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context.SendRequestAsync(requests[1]));
|
||||
await _context.SendRequestAsync(requests[1]).ConfigureAwait(false));
|
||||
|
||||
// 验证回滚机制被触发
|
||||
Assert.That(sagaData.CompletedSteps, Is.EqualTo(new[] { 1 })); // 只有第一步完成
|
||||
@ -206,7 +208,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
[Test]
|
||||
public async Task Request_Chaining_With_Dependencies_Should_Work_Correctly()
|
||||
{
|
||||
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest());
|
||||
var chainResult = await _context!.SendRequestAsync(new TestChainStartRequest()).ConfigureAwait(false);
|
||||
|
||||
Assert.That(chainResult, Is.EqualTo("Chain completed: Step1 -> Step2 -> Step3"));
|
||||
}
|
||||
@ -218,7 +220,7 @@ public class MediatorAdvancedFeaturesTests
|
||||
var request = new TestExternalServiceRequest { TimeoutMs = 1000 };
|
||||
|
||||
Assert.ThrowsAsync<TaskCanceledException>(async () =>
|
||||
await _context!.SendRequestAsync(request, cts.Token));
|
||||
await _context!.SendRequestAsync(request, cts.Token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -227,13 +229,15 @@ public class MediatorAdvancedFeaturesTests
|
||||
var testData = new List<string>();
|
||||
var request = new TestDatabaseRequest { Data = "test data", Storage = testData };
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Data saved successfully"));
|
||||
Assert.That(testData, Contains.Item("test data"));
|
||||
}
|
||||
}
|
||||
|
||||
// 这些高级特性测试需要把一组仅供当前文件使用的辅助类型共置,避免拆成多个噪声文件。
|
||||
#pragma warning disable MA0048
|
||||
#region Advanced Test Classes
|
||||
|
||||
public sealed class TestRetryRequestHandler : IRequestHandler<TestRetryRequest, string>
|
||||
@ -329,7 +333,7 @@ public sealed class TestChainStartRequestHandler : IRequestHandler<TestChainStar
|
||||
public async ValueTask<string> Handle(TestChainStartRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟链式调用
|
||||
await Task.Delay(10, cancellationToken);
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
return "Chain completed: Step1 -> Step2 -> Step3";
|
||||
}
|
||||
}
|
||||
@ -338,7 +342,7 @@ public sealed class TestExternalServiceRequestHandler : IRequestHandler<TestExte
|
||||
{
|
||||
public async ValueTask<string> Handle(TestExternalServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.TimeoutMs, cancellationToken);
|
||||
await Task.Delay(request.TimeoutMs, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return "External service response";
|
||||
}
|
||||
@ -378,7 +382,7 @@ public sealed class TestValidatedRequestHandler : IRequestHandler<TestValidatedR
|
||||
// 验证输入
|
||||
if (request.Value < 0)
|
||||
{
|
||||
throw new ArgumentException("Value must be non-negative", nameof(request.Value));
|
||||
throw new ArgumentException("Value must be non-negative", nameof(request));
|
||||
}
|
||||
|
||||
return new ValueTask<string>($"Value: {request.Value}");
|
||||
@ -406,7 +410,7 @@ public sealed class TestPerformanceRequestHandler : IRequestHandler<TestPerforma
|
||||
{
|
||||
public async ValueTask<int> Handle(TestPerformanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.ProcessingTimeMs, cancellationToken);
|
||||
await Task.Delay(request.ProcessingTimeMs, cancellationToken).ConfigureAwait(false);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
@ -503,3 +507,4 @@ public sealed record TestDatabaseRequest : IRequest<string>
|
||||
}
|
||||
|
||||
#endregion
|
||||
#pragma warning restore MA0048
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
@ -62,7 +63,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestContextAwareHandler.LastContext = _context; // 直接设置
|
||||
var request = new TestContextAwareRequest();
|
||||
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestContextAwareHandler.LastContext, Is.Not.Null);
|
||||
Assert.That(TestContextAwareHandler.LastContext, Is.SameAs(_context));
|
||||
@ -74,7 +75,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestServiceRetrievalHandler.LastRetrievedService = null;
|
||||
var request = new TestServiceRetrievalRequest();
|
||||
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.Not.Null);
|
||||
Assert.That(TestServiceRetrievalHandler.LastRetrievedService, Is.InstanceOf<TestService>());
|
||||
@ -86,7 +87,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestNestedRequestHandler2.ExecutionCount = 0;
|
||||
var request = new TestNestedRequest { Depth = 1 }; // 简化为深度1
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Nested execution completed at depth 1"));
|
||||
Assert.That(TestNestedRequestHandler2.ExecutionCount, Is.EqualTo(1));
|
||||
@ -99,7 +100,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestLifecycleHandler.DisposalCount = 0;
|
||||
|
||||
var request = new TestLifecycleRequest();
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
// 验证生命周期管理
|
||||
Assert.That(TestLifecycleHandler.InitializationCount, Is.EqualTo(1));
|
||||
@ -116,14 +117,14 @@ public class MediatorArchitectureIntegrationTests
|
||||
.Select(async i =>
|
||||
{
|
||||
var request = new TestScopedServiceRequest { RequestId = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
lock (results)
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证每个请求都得到了独立的scope实例
|
||||
Assert.That(results.Distinct().Count(), Is.EqualTo(10));
|
||||
@ -135,7 +136,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
var request = new TestErrorPropagationRequest();
|
||||
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
|
||||
Assert.That(ex!.Message, Is.EqualTo("Test error from handler"));
|
||||
Assert.That(ex.Data["RequestId"], Is.Not.Null);
|
||||
@ -148,7 +149,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
var request = new TestExceptionRequest();
|
||||
|
||||
Assert.ThrowsAsync<DivideByZeroException>(async () =>
|
||||
await _context!.SendRequestAsync(request));
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false));
|
||||
|
||||
// 验证异常被捕获和记录
|
||||
Assert.That(TestExceptionHandler.LastException, Is.Not.Null);
|
||||
@ -164,7 +165,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var request = new TestPerformanceRequest2 { Id = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(i));
|
||||
}
|
||||
|
||||
@ -188,7 +189,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var request = new TestUncachedRequest { Id = i };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
uncachedTimes.Add(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
@ -198,7 +199,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var request = new TestCachedRequest { Id = i };
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
stopwatch.Stop();
|
||||
cachedTimes.Add(stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
@ -224,12 +225,12 @@ public class MediatorArchitectureIntegrationTests
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
var request = new TestConcurrentRequest { RequestId = requestId, OrderTracker = executionOrder };
|
||||
return await _context!.SendRequestAsync(request);
|
||||
return await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
});
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证所有请求都成功完成
|
||||
Assert.That(results.Length, Is.EqualTo(concurrentRequests));
|
||||
@ -253,10 +254,10 @@ public class MediatorArchitectureIntegrationTests
|
||||
SharedState = sharedState,
|
||||
Increment = 1
|
||||
};
|
||||
await _context!.SendRequestAsync(request);
|
||||
await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证最终状态正确(20个并发操作,每个+1)
|
||||
Assert.That(sharedState.Counter, Is.EqualTo(concurrentOperations));
|
||||
@ -269,7 +270,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
TestIntegrationHandler.LastSystemCall = null;
|
||||
var request = new TestIntegrationRequest();
|
||||
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo("Integration successful"));
|
||||
Assert.That(TestIntegrationHandler.LastSystemCall, Is.EqualTo("System executed"));
|
||||
@ -285,7 +286,7 @@ public class MediatorArchitectureIntegrationTests
|
||||
|
||||
// 使用Mediator
|
||||
var mediatorRequest = new TestMediatorRequest { Value = 42 };
|
||||
var result = await _context.SendRequestAsync(mediatorRequest);
|
||||
var result = await _context.SendRequestAsync(mediatorRequest).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
|
||||
// 验证两者可以共存
|
||||
@ -296,8 +297,8 @@ public class MediatorArchitectureIntegrationTests
|
||||
[Test]
|
||||
public async Task ContextAware_Handler_Should_Use_A_Fresh_Instance_Per_Request()
|
||||
{
|
||||
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest());
|
||||
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest());
|
||||
var firstResult = await _context!.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
|
||||
var secondResult = await _context.SendRequestAsync(new TestPerDispatchContextAwareRequest()).ConfigureAwait(false);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@ -306,312 +307,312 @@ public class MediatorArchitectureIntegrationTests
|
||||
Assert.That(TestPerDispatchContextAwareHandler.Contexts, Has.All.SameAs(_context));
|
||||
});
|
||||
}
|
||||
}
|
||||
#region Integration Test Classes
|
||||
|
||||
#region Integration Test Classes
|
||||
|
||||
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestContextAwareRequestHandler : IRequestHandler<TestContextAwareRequest, string>
|
||||
{
|
||||
// 保持测试中设置的上下文,不要重置为null
|
||||
return new ValueTask<string>("Context accessed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
|
||||
return new ValueTask<string>("Service retrieved");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestNestedRequestHandler2.ExecutionCount++;
|
||||
|
||||
if (request.Depth >= 1) // 简化条件
|
||||
public ValueTask<string> Handle(TestContextAwareRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 保持测试中设置的上下文,不要重置为null
|
||||
return new ValueTask<string>("Context accessed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestServiceRetrievalRequestHandler : IRequestHandler<TestServiceRetrievalRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestServiceRetrievalRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestServiceRetrievalHandler.LastRetrievedService = new TestService();
|
||||
return new ValueTask<string>("Service retrieved");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestNestedRequestHandler : IRequestHandler<TestNestedRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestNestedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestNestedRequestHandler2.ExecutionCount++;
|
||||
// 模拟嵌套调用
|
||||
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
|
||||
}
|
||||
|
||||
return new ValueTask<string>($"Nested execution completed at depth {request.Depth}");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestLifecycleRequestHandler : IRequestHandler<TestLifecycleRequest, string>
|
||||
{
|
||||
TestLifecycleHandler.InitializationCount++;
|
||||
// 模拟一些工作
|
||||
TestLifecycleHandler.DisposalCount++;
|
||||
return new ValueTask<string>("Lifecycle managed");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟返回请求ID
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ex = new InvalidOperationException("Test error from handler");
|
||||
ex.Data["RequestId"] = Guid.NewGuid();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
|
||||
throw TestExceptionHandler.LastException;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟一些处理时间
|
||||
Task.Delay(5, cancellationToken).Wait(cancellationToken);
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
|
||||
{
|
||||
private static readonly Dictionary<int, int> _cache = new();
|
||||
|
||||
public ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cache.TryGetValue(request.Id, out var cachedValue))
|
||||
public ValueTask<string> Handle(TestLifecycleRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(cachedValue);
|
||||
TestLifecycleHandler.InitializationCount++;
|
||||
// 模拟一些工作
|
||||
TestLifecycleHandler.DisposalCount++;
|
||||
return new ValueTask<string>("Lifecycle managed");
|
||||
}
|
||||
|
||||
// 模拟处理时间
|
||||
Task.Delay(10, cancellationToken).Wait(cancellationToken);
|
||||
var newValue = request.Id;
|
||||
_cache[request.Id] = newValue;
|
||||
return new ValueTask<int>(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestScopedServiceRequestHandler : IRequestHandler<TestScopedServiceRequest, int>
|
||||
{
|
||||
lock (request.OrderTracker)
|
||||
public ValueTask<int> Handle(TestScopedServiceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.OrderTracker.Add(request.RequestId);
|
||||
// 模拟返回请求ID
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestErrorPropagationRequestHandler : IRequestHandler<TestErrorPropagationRequest, string>
|
||||
{
|
||||
request.SharedState.Counter += request.Increment;
|
||||
return new ValueTask<string>("State modified");
|
||||
public ValueTask<string> Handle(TestErrorPropagationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ex = new InvalidOperationException("Test error from handler");
|
||||
ex.Data["RequestId"] = Guid.NewGuid();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestExceptionRequestHandler : IRequestHandler<TestExceptionRequest, string>
|
||||
{
|
||||
TestIntegrationHandler.LastSystemCall = "System executed";
|
||||
return new ValueTask<string>("Integration successful");
|
||||
public ValueTask<string> Handle(TestExceptionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestExceptionHandler.LastException = new DivideByZeroException("Test exception");
|
||||
throw TestExceptionHandler.LastException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestPerformanceRequest2Handler : IRequestHandler<TestPerformanceRequest2, int>
|
||||
{
|
||||
return new ValueTask<int>(request.Value);
|
||||
public ValueTask<int> Handle(TestPerformanceRequest2 request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
|
||||
/// </summary>
|
||||
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
|
||||
IRequestHandler<TestPerDispatchContextAwareRequest, int>
|
||||
{
|
||||
private static int _nextInstanceId;
|
||||
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
|
||||
|
||||
public static List<IArchitectureContext?> Contexts { get; } = [];
|
||||
public static List<int> SeenInstanceIds { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 记录当前实例编号与收到的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="request">请求实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前处理器实例编号。</returns>
|
||||
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
|
||||
public sealed class TestUncachedRequestHandler : IRequestHandler<TestUncachedRequest, int>
|
||||
{
|
||||
Contexts.Add(Context);
|
||||
SeenInstanceIds.Add(_instanceId);
|
||||
return ValueTask.FromResult(_instanceId);
|
||||
public async ValueTask<int> Handle(TestUncachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 模拟一些处理时间
|
||||
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestCachedRequestHandler : IRequestHandler<TestCachedRequest, int>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<int, int> _cache = new();
|
||||
|
||||
public async ValueTask<int> Handle(TestCachedRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cache.TryGetValue(request.Id, out var cachedValue))
|
||||
{
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// 模拟处理时间
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
|
||||
return _cache.GetOrAdd(request.Id, static id => id);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestConcurrentRequestHandler : IRequestHandler<TestConcurrentRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestConcurrentRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (request.OrderTracker)
|
||||
{
|
||||
request.OrderTracker.Add(request.RequestId);
|
||||
}
|
||||
|
||||
return new ValueTask<int>(request.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestStateModificationRequestHandler : IRequestHandler<TestStateModificationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestStateModificationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.SharedState.IncrementBy(request.Increment);
|
||||
return new ValueTask<string>("State modified");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestIntegrationRequestHandler : IRequestHandler<TestIntegrationRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(TestIntegrationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TestIntegrationHandler.LastSystemCall = "System executed";
|
||||
return new ValueTask<string>("Integration successful");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TestMediatorRequestHandler : IRequestHandler<TestMediatorRequest, int>
|
||||
{
|
||||
public ValueTask<int> Handle(TestMediatorRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<int>(request.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置跨测试共享的实例跟踪状态。
|
||||
/// 用于验证自动扫描到的上下文感知处理器会按请求创建新实例。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
public sealed class TestPerDispatchContextAwareHandler : ContextAwareBase,
|
||||
IRequestHandler<TestPerDispatchContextAwareRequest, int>
|
||||
{
|
||||
Contexts.Clear();
|
||||
SeenInstanceIds.Clear();
|
||||
_nextInstanceId = 0;
|
||||
}
|
||||
}
|
||||
private static int _nextInstanceId;
|
||||
private static readonly List<IArchitectureContext?> TrackedContexts = [];
|
||||
private static readonly List<int> TrackedInstanceIds = [];
|
||||
private readonly int _instanceId = Interlocked.Increment(ref _nextInstanceId);
|
||||
|
||||
public sealed record TestContextAwareRequest : IRequest<string>;
|
||||
public static IReadOnlyList<IArchitectureContext?> Contexts => TrackedContexts;
|
||||
public static IReadOnlyList<int> SeenInstanceIds => TrackedInstanceIds;
|
||||
|
||||
public static class TestContextAwareHandler
|
||||
{
|
||||
public static IArchitectureContext? LastContext { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 记录当前实例编号与收到的架构上下文。
|
||||
/// </summary>
|
||||
/// <param name="request">请求实例。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>当前处理器实例编号。</returns>
|
||||
public ValueTask<int> Handle(TestPerDispatchContextAwareRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
TrackedContexts.Add(Context);
|
||||
TrackedInstanceIds.Add(_instanceId);
|
||||
return ValueTask.FromResult(_instanceId);
|
||||
}
|
||||
|
||||
public sealed record TestServiceRetrievalRequest : IRequest<string>;
|
||||
|
||||
public static class TestServiceRetrievalHandler
|
||||
{
|
||||
public static object? LastRetrievedService { get; set; }
|
||||
}
|
||||
|
||||
public class TestService
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
public sealed record TestNestedRequest : IRequest<string>
|
||||
{
|
||||
public int Depth { get; init; }
|
||||
}
|
||||
|
||||
public static class TestNestedRequestHandler2
|
||||
{
|
||||
public static int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
// 生命周期相关类
|
||||
public sealed record TestLifecycleRequest : IRequest<string>;
|
||||
|
||||
public static class TestLifecycleHandler
|
||||
{
|
||||
public static int InitializationCount { get; set; }
|
||||
public static int DisposalCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestScopedServiceRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
}
|
||||
|
||||
// 错误处理相关类
|
||||
public sealed record TestErrorPropagationRequest : IRequest<string>;
|
||||
|
||||
public static class TestExceptionHandler
|
||||
{
|
||||
public static Exception? LastException { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestExceptionRequest : IRequest<string>;
|
||||
|
||||
// 性能测试相关类
|
||||
public sealed record TestPerformanceRequest2 : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestUncachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestCachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
// 并发测试相关类
|
||||
public class SharedState
|
||||
{
|
||||
public int Counter { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestConcurrentRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
public List<int> OrderTracker { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed record TestStateModificationRequest : IRequest<string>
|
||||
{
|
||||
public SharedState SharedState { get; init; } = null!;
|
||||
public int Increment { get; init; }
|
||||
}
|
||||
|
||||
// 集成测试相关类
|
||||
public static class TestIntegrationHandler
|
||||
{
|
||||
public static string? LastSystemCall { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestIntegrationRequest : IRequest<string>;
|
||||
|
||||
public sealed record TestMediatorRequest : IRequest<int>
|
||||
{
|
||||
public int Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
|
||||
/// </summary>
|
||||
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
|
||||
|
||||
// 传统命令用于混合测试
|
||||
public class TestTraditionalCommand : ICommand
|
||||
{
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
public void Execute() => Executed = true;
|
||||
|
||||
public void SetContext(IArchitectureContext context)
|
||||
{
|
||||
/// <summary>
|
||||
/// 重置跨测试共享的实例跟踪状态。
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
TrackedContexts.Clear();
|
||||
TrackedInstanceIds.Clear();
|
||||
_nextInstanceId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public IArchitectureContext GetContext() => null!;
|
||||
}
|
||||
public sealed record TestContextAwareRequest : IRequest<string>;
|
||||
|
||||
#endregion
|
||||
public static class TestContextAwareHandler
|
||||
{
|
||||
public static IArchitectureContext? LastContext { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestServiceRetrievalRequest : IRequest<string>;
|
||||
|
||||
public static class TestServiceRetrievalHandler
|
||||
{
|
||||
public static object? LastRetrievedService { get; set; }
|
||||
}
|
||||
|
||||
public class TestService
|
||||
{
|
||||
public string Id { get; } = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
public sealed record TestNestedRequest : IRequest<string>
|
||||
{
|
||||
public int Depth { get; init; }
|
||||
}
|
||||
|
||||
public static class TestNestedRequestHandler2
|
||||
{
|
||||
public static int ExecutionCount { get; set; }
|
||||
}
|
||||
|
||||
// 生命周期相关类
|
||||
public sealed record TestLifecycleRequest : IRequest<string>;
|
||||
|
||||
public static class TestLifecycleHandler
|
||||
{
|
||||
public static int InitializationCount { get; set; }
|
||||
public static int DisposalCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestScopedServiceRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
}
|
||||
|
||||
// 错误处理相关类
|
||||
public sealed record TestErrorPropagationRequest : IRequest<string>;
|
||||
|
||||
public static class TestExceptionHandler
|
||||
{
|
||||
public static Exception? LastException { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestExceptionRequest : IRequest<string>;
|
||||
|
||||
// 性能测试相关类
|
||||
public sealed record TestPerformanceRequest2 : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestUncachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TestCachedRequest : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
}
|
||||
|
||||
// 并发测试相关类
|
||||
public class SharedState
|
||||
{
|
||||
private int _counter;
|
||||
|
||||
public int Counter => _counter;
|
||||
|
||||
public void IncrementBy(int increment)
|
||||
{
|
||||
Interlocked.Add(ref _counter, increment);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TestConcurrentRequest : IRequest<int>
|
||||
{
|
||||
public int RequestId { get; init; }
|
||||
public ICollection<int> OrderTracker { get; init; } = new List<int>();
|
||||
}
|
||||
|
||||
public sealed record TestStateModificationRequest : IRequest<string>
|
||||
{
|
||||
public SharedState SharedState { get; init; } = null!;
|
||||
public int Increment { get; init; }
|
||||
}
|
||||
|
||||
// 集成测试相关类
|
||||
public static class TestIntegrationHandler
|
||||
{
|
||||
public static string? LastSystemCall { get; set; }
|
||||
}
|
||||
|
||||
public sealed record TestIntegrationRequest : IRequest<string>;
|
||||
|
||||
public sealed record TestMediatorRequest : IRequest<int>
|
||||
{
|
||||
public int Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证每次请求分发都会获得新的上下文感知处理器实例。
|
||||
/// </summary>
|
||||
public sealed record TestPerDispatchContextAwareRequest : IRequest<int>;
|
||||
|
||||
// 传统命令用于混合测试
|
||||
public class TestTraditionalCommand : ICommand
|
||||
{
|
||||
public bool Executed { get; private set; }
|
||||
|
||||
public void Execute() => Executed = true;
|
||||
|
||||
public void SetContext(IArchitectureContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public IArchitectureContext GetContext() => null!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendRequestAsync_Should_ReturnResult_When_Request_IsValid()
|
||||
{
|
||||
var testRequest = new TestRequest { Value = 42 };
|
||||
var result = await _context!.SendRequestAsync(testRequest);
|
||||
var result = await _context!.SendRequestAsync(testRequest).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
@ -98,7 +98,7 @@ public class MediatorComprehensiveTests
|
||||
public void SendRequestAsync_Should_ThrowArgumentNullException_When_Request_IsNull()
|
||||
{
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () =>
|
||||
await _context!.SendRequestAsync<int>(null!));
|
||||
await _context!.SendRequestAsync<int>(null!).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -122,8 +122,8 @@ public class MediatorComprehensiveTests
|
||||
TestNotificationHandler.LastReceivedMessage = null;
|
||||
var notification = new TestNotification { Message = "test" };
|
||||
|
||||
await _context!.PublishAsync(notification);
|
||||
await Task.Delay(100);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("test"));
|
||||
}
|
||||
@ -138,7 +138,7 @@ public class MediatorComprehensiveTests
|
||||
var stream = _context!.CreateStream(testStreamRequest);
|
||||
|
||||
var results = new List<int>();
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -153,7 +153,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendAsync_CommandWithoutResult_Should_Execute_When_Command_IsValid()
|
||||
{
|
||||
var testCommand = new TestCommand { ShouldExecute = true };
|
||||
await _context!.SendAsync(testCommand);
|
||||
await _context!.SendAsync(testCommand).ConfigureAwait(false);
|
||||
|
||||
Assert.That(testCommand.Executed, Is.True);
|
||||
}
|
||||
@ -165,7 +165,7 @@ public class MediatorComprehensiveTests
|
||||
public async Task SendAsync_CommandWithResult_Should_ReturnResult_When_Command_IsValid()
|
||||
{
|
||||
var testCommand = new TestCommandWithResult { ResultValue = 42 };
|
||||
var result = await _context!.SendAsync(testCommand);
|
||||
var result = await _context!.SendAsync(testCommand).ConfigureAwait(false);
|
||||
|
||||
Assert.That(result, Is.EqualTo(42));
|
||||
}
|
||||
@ -198,7 +198,7 @@ public class MediatorComprehensiveTests
|
||||
var testRequest = new TestRequest { Value = 42 };
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await contextWithoutHandlers.SendRequestAsync(testRequest));
|
||||
await contextWithoutHandlers.SendRequestAsync(testRequest).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -213,8 +213,8 @@ public class MediatorComprehensiveTests
|
||||
TestNotificationHandler3.LastReceivedMessage = null;
|
||||
|
||||
var notification = new TestNotification { Message = "multi-handler test" };
|
||||
await _context!.PublishAsync(notification);
|
||||
await Task.Delay(100);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
|
||||
// 验证所有处理器都被调用
|
||||
Assert.That(TestNotificationHandler.LastReceivedMessage, Is.EqualTo("multi-handler test"));
|
||||
@ -233,7 +233,7 @@ public class MediatorComprehensiveTests
|
||||
|
||||
// 应该在50ms后被取消
|
||||
Assert.ThrowsAsync<TaskCanceledException>(async () =>
|
||||
await _context!.SendRequestAsync(longRequest, cts.Token));
|
||||
await _context!.SendRequestAsync(longRequest, cts.Token).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -251,7 +251,7 @@ public class MediatorComprehensiveTests
|
||||
// 流应该在100ms后被取消(TaskCanceledException 继承自 OperationCanceledException)
|
||||
Assert.CatchAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -277,7 +277,7 @@ public class MediatorComprehensiveTests
|
||||
tasks.Add(_context!.SendRequestAsync(request).AsTask());
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
// 验证所有结果都正确返回
|
||||
Assert.That(results.Length, Is.EqualTo(requestCount));
|
||||
@ -293,7 +293,7 @@ public class MediatorComprehensiveTests
|
||||
var faultyRequest = new TestFaultyRequest();
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _context!.SendRequestAsync(faultyRequest));
|
||||
await _context!.SendRequestAsync(faultyRequest).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -306,8 +306,8 @@ public class MediatorComprehensiveTests
|
||||
var command1 = new TestModifyDataCommand { Data = sharedData, Value = 10 };
|
||||
var command2 = new TestModifyDataCommand { Data = sharedData, Value = 20 };
|
||||
|
||||
await _context!.SendAsync(command1);
|
||||
await _context.SendAsync(command2);
|
||||
await _context!.SendAsync(command1).ConfigureAwait(false);
|
||||
await _context.SendAsync(command2).ConfigureAwait(false);
|
||||
|
||||
// 验证数据被正确修改
|
||||
Assert.That(sharedData.Value, Is.EqualTo(30)); // 10 + 20
|
||||
@ -331,10 +331,10 @@ public class MediatorComprehensiveTests
|
||||
|
||||
foreach (var notification in notifications)
|
||||
{
|
||||
await _context!.PublishAsync(notification);
|
||||
await _context!.PublishAsync(notification).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(200); // 等待所有处理完成
|
||||
await Task.Delay(200).ConfigureAwait(false); // 等待所有处理完成
|
||||
|
||||
// 验证接收顺序与发送顺序一致
|
||||
Assert.That(receivedOrder.Count, Is.EqualTo(3));
|
||||
@ -358,7 +358,7 @@ public class MediatorComprehensiveTests
|
||||
var stream = _context!.CreateStream(filterRequest);
|
||||
var results = new List<int>();
|
||||
|
||||
await foreach (var item in stream)
|
||||
await foreach (var item in stream.ConfigureAwait(false))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
@ -377,7 +377,7 @@ public class MediatorComprehensiveTests
|
||||
var invalidCommand = new TestValidatedCommand { Name = "" }; // 无效:空字符串
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(async () =>
|
||||
await _context!.SendAsync(invalidCommand));
|
||||
await _context!.SendAsync(invalidCommand).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -392,7 +392,7 @@ public class MediatorComprehensiveTests
|
||||
for (int i = 0; i < iterations; i++)
|
||||
{
|
||||
var request = new TestRequest { Value = i };
|
||||
var result = await _context!.SendRequestAsync(request);
|
||||
var result = await _context!.SendRequestAsync(request).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(i));
|
||||
}
|
||||
|
||||
@ -417,15 +417,13 @@ public class MediatorComprehensiveTests
|
||||
|
||||
// 使用自有 CQRS 方式
|
||||
var mediatorCommand = new TestCommandWithResult { ResultValue = 999 };
|
||||
var result = await _context.SendAsync(mediatorCommand);
|
||||
var result = await _context.SendAsync(mediatorCommand).ConfigureAwait(false);
|
||||
Assert.That(result, Is.EqualTo(999));
|
||||
|
||||
// 验证两者可以同时工作
|
||||
Assert.That(legacyCommand.Executed, Is.True);
|
||||
Assert.That(result, Is.EqualTo(999));
|
||||
}
|
||||
}
|
||||
|
||||
#region Advanced Test Classes for CQRS Features
|
||||
|
||||
public sealed record TestLongRunningRequest : IRequest<string>
|
||||
@ -437,7 +435,7 @@ public sealed class TestLongRunningRequestHandler : IRequestHandler<TestLongRunn
|
||||
{
|
||||
public async ValueTask<string> Handle(TestLongRunningRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(request.DelayMs, cancellationToken);
|
||||
await Task.Delay(request.DelayMs, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return "Completed";
|
||||
}
|
||||
@ -458,7 +456,7 @@ public sealed class TestLongStreamRequestHandler : IStreamRequestHandler<TestLon
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return i;
|
||||
await Task.Delay(10, cancellationToken); // 模拟处理延迟
|
||||
await Task.Delay(10, cancellationToken).ConfigureAwait(false); // 模拟处理延迟
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,7 +494,7 @@ public sealed class TestModifyDataCommandHandler : IRequestHandler<TestModifyDat
|
||||
public sealed record TestCachingQuery : IRequest<string>
|
||||
{
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public Dictionary<string, string> Cache { get; init; } = new();
|
||||
public IDictionary<string, string> Cache { get; init; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class TestCachingQueryHandler : IRequestHandler<TestCachingQuery, string>
|
||||
@ -522,7 +520,7 @@ public sealed record TestOrderedNotification : INotification
|
||||
|
||||
public sealed class TestOrderedNotificationHandler : INotificationHandler<TestOrderedNotification>
|
||||
{
|
||||
public static List<string> ReceivedMessages { get; set; } = new();
|
||||
public static ICollection<string> ReceivedMessages { get; set; } = new List<string>();
|
||||
|
||||
public ValueTask Handle(TestOrderedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
@ -590,7 +588,7 @@ public sealed class TestValidatedCommandHandler : IRequestHandler<TestValidatedC
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentException($"Name cannot be empty {nameof(request.Name)}");
|
||||
throw new ArgumentException("Name cannot be empty.", nameof(request));
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(Unit.Value);
|
||||
@ -719,3 +717,5 @@ public sealed class TestStreamRequestHandler : IStreamRequestHandler<TestStreamR
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
147
GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs
Normal file
147
GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GFramework.Game.Config;
|
||||
|
||||
namespace GFramework.Game.Tests.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 验证内部 schema 运行时模型会在构造阶段拒绝无效状态,
|
||||
/// 避免调用方把不一致的约束对象继续传入加载器和校验器。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class YamlConfigModelContractTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证枚举允许值模型会拒绝空白比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AllowedValue_Should_Reject_Whitespace_Comparable_Value()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigAllowedValue(" ", "visible"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证枚举允许值模型会保留空对象等合法结构产生的空比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AllowedValue_Should_Accept_Empty_Comparable_Value()
|
||||
{
|
||||
var allowedValue = new YamlConfigAllowedValue(string.Empty, "{}");
|
||||
|
||||
Assert.That(allowedValue.ComparableValue, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证常量约束模型会拒绝空白比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConstantValue_Should_Reject_Whitespace_Comparable_Value()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigConstantValue(" ", "\"visible\""));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证常量约束模型会保留空对象等合法结构产生的空比较键。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConstantValue_Should_Accept_Empty_Comparable_Value()
|
||||
{
|
||||
var constantValue = new YamlConfigConstantValue(string.Empty, "{}");
|
||||
|
||||
Assert.That(constantValue.ComparableValue, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 contains 约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ArrayContainsConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
var itemNode = CreateStringNode();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, -1, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayContainsConstraints(itemNode, null, -1));
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigArrayContainsConstraints(itemNode, 3, 2));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ArrayConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(-1, null, false, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new YamlConfigArrayConstraints(null, -1, false, null));
|
||||
Assert.Throws<ArgumentException>(() => new YamlConfigArrayConstraints(4, 3, false, null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证对象约束模型会在构造阶段拦截负值和反向区间。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ObjectConstraints_Should_Reject_Invalid_Bounds()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new YamlConfigObjectConstraints(-1, null, null, null, null, null));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new YamlConfigObjectConstraints(null, -1, null, null, null, null));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigObjectConstraints(5, 4, null, null, null, null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串约束模型要求正则原文与预编译正则成对出现。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void StringConstraints_Should_Require_Pattern_And_Regex_To_Be_Paired()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigStringConstraints(null, null, "value", null, null));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new YamlConfigStringConstraints(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new Regex("value", RegexOptions.None, TimeSpan.FromSeconds(1)),
|
||||
null));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 模型会复制引用表集合,避免外部可变集合继续污染内部状态。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Schema_Should_Copy_Referenced_Table_Names()
|
||||
{
|
||||
var referencedTableNames = new List<string> { "item" };
|
||||
var schema = new YamlConfigSchema("monster.schema.json", CreateStringNode(), referencedTableNames);
|
||||
|
||||
referencedTableNames.Add("weapon");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(schema.ReferencedTableNames, Is.EqualTo(new[] { "item" }));
|
||||
Assert.That(schema.ReferencedTableNames, Is.Not.SameAs(referencedTableNames));
|
||||
});
|
||||
}
|
||||
|
||||
private static YamlConfigSchemaNode CreateStringNode()
|
||||
{
|
||||
return YamlConfigSchemaNode.CreateScalar(
|
||||
YamlConfigSchemaPropertyType.String,
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
schemaPathHint: "tests.schema.json");
|
||||
}
|
||||
}
|
||||
39
GFramework.Game/Config/YamlConfigAllowedValue.cs
Normal file
39
GFramework.Game/Config/YamlConfigAllowedValue.cs
Normal file
@ -0,0 +1,39 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个节点上声明的单个 <c>enum</c> 候选值。
|
||||
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigAllowedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个枚举候选值模型。
|
||||
/// </summary>
|
||||
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
|
||||
/// <param name="displayValue">用于诊断输出的原始 JSON 文本。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigAllowedValue(string comparableValue, string displayValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(comparableValue);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
|
||||
if (comparableValue.Length > 0 &&
|
||||
string.IsNullOrWhiteSpace(comparableValue))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
|
||||
}
|
||||
|
||||
ComparableValue = comparableValue;
|
||||
DisplayValue = displayValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于运行时比较的稳定键。
|
||||
/// </summary>
|
||||
public string ComparableValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断输出的原始 JSON 文本。
|
||||
/// </summary>
|
||||
public string DisplayValue { get; }
|
||||
}
|
||||
66
GFramework.Game/Config/YamlConfigArrayConstraints.cs
Normal file
66
GFramework.Game/Config/YamlConfigArrayConstraints.cs
Normal file
@ -0,0 +1,66 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。
|
||||
/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigArrayConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数组约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minItems">最小元素数量约束。</param>
|
||||
/// <param name="maxItems">最大元素数量约束。</param>
|
||||
/// <param name="uniqueItems">是否要求数组元素唯一。</param>
|
||||
/// <param name="containsConstraints">数组 contains 约束;未声明时为空。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minItems"/> 或 <paramref name="maxItems"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minItems"/> 大于 <paramref name="maxItems"/> 时抛出。</exception>
|
||||
public YamlConfigArrayConstraints(
|
||||
int? minItems,
|
||||
int? maxItems,
|
||||
bool uniqueItems,
|
||||
YamlConfigArrayContainsConstraints? containsConstraints)
|
||||
{
|
||||
if (minItems is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minItems), minItems, "minItems 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxItems is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxItems), maxItems, "maxItems 不能为负数。");
|
||||
}
|
||||
|
||||
if (minItems.HasValue &&
|
||||
maxItems.HasValue &&
|
||||
minItems.Value > maxItems.Value)
|
||||
{
|
||||
throw new ArgumentException("minItems 不能大于 maxItems。", nameof(minItems));
|
||||
}
|
||||
|
||||
MinItems = minItems;
|
||||
MaxItems = maxItems;
|
||||
UniqueItems = uniqueItems;
|
||||
ContainsConstraints = containsConstraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小元素数量约束。
|
||||
/// </summary>
|
||||
public int? MinItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大元素数量约束。
|
||||
/// </summary>
|
||||
public int? MaxItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否要求数组元素唯一。
|
||||
/// </summary>
|
||||
public bool UniqueItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组 contains 约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigArrayContainsConstraints? ContainsConstraints { get; }
|
||||
}
|
||||
60
GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs
Normal file
60
GFramework.Game/Config/YamlConfigArrayContainsConstraints.cs
Normal file
@ -0,0 +1,60 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示数组节点声明的 <c>contains</c> 匹配约束。
|
||||
/// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigArrayContainsConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数组 contains 约束模型。
|
||||
/// </summary>
|
||||
/// <param name="containsNode">contains 子 schema。</param>
|
||||
/// <param name="minContains">最小匹配数量;为 <see langword="null" /> 时按 JSON Schema 语义默认 1。</param>
|
||||
/// <param name="maxContains">最大匹配数量。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="containsNode"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minContains"/> 或 <paramref name="maxContains"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minContains"/> 大于 <paramref name="maxContains"/> 时抛出。</exception>
|
||||
public YamlConfigArrayContainsConstraints(
|
||||
YamlConfigSchemaNode containsNode,
|
||||
int? minContains,
|
||||
int? maxContains)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(containsNode);
|
||||
if (minContains is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minContains), minContains, "minContains 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxContains is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxContains), maxContains, "maxContains 不能为负数。");
|
||||
}
|
||||
|
||||
if (minContains.HasValue &&
|
||||
maxContains.HasValue &&
|
||||
minContains.Value > maxContains.Value)
|
||||
{
|
||||
throw new ArgumentException("minContains 不能大于 maxContains。", nameof(minContains));
|
||||
}
|
||||
|
||||
ContainsNode = containsNode;
|
||||
MinContains = minContains;
|
||||
MaxContains = maxContains;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 contains 子 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode ContainsNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。
|
||||
/// </summary>
|
||||
public int? MinContains { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大匹配数量。
|
||||
/// </summary>
|
||||
public int? MaxContains { get; }
|
||||
}
|
||||
42
GFramework.Game/Config/YamlConfigConditionalSchemas.cs
Normal file
42
GFramework.Game/Config/YamlConfigConditionalSchemas.cs
Normal file
@ -0,0 +1,42 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对象节点上声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 三个分支都共享父对象已声明字段集合,不会把分支 schema 扩展成新的生成类型形状。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigConditionalSchemas
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化条件分支约束模型。
|
||||
/// </summary>
|
||||
/// <param name="ifSchema">条件判断 schema。</param>
|
||||
/// <param name="thenSchema">条件命中时需要满足的 schema。</param>
|
||||
/// <param name="elseSchema">条件未命中时需要满足的 schema。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="ifSchema"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public YamlConfigConditionalSchemas(
|
||||
YamlConfigSchemaNode ifSchema,
|
||||
YamlConfigSchemaNode? thenSchema,
|
||||
YamlConfigSchemaNode? elseSchema)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ifSchema);
|
||||
|
||||
IfSchema = ifSchema;
|
||||
ThenSchema = thenSchema;
|
||||
ElseSchema = elseSchema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件判断 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode IfSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件命中时需要满足的 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ThenSchema { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取条件未命中时需要满足的 schema。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ElseSchema { get; }
|
||||
}
|
||||
39
GFramework.Game/Config/YamlConfigConstantValue.cs
Normal file
39
GFramework.Game/Config/YamlConfigConstantValue.cs
Normal file
@ -0,0 +1,39 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个节点上声明的 <c>const</c> 约束。
|
||||
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigConstantValue
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化常量约束模型。
|
||||
/// </summary>
|
||||
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
|
||||
/// <param name="displayValue">用于诊断输出的原始常量文本。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="comparableValue"/> 或 <paramref name="displayValue"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="comparableValue"/> 虽然非空但仅包含空白字符,或 <paramref name="displayValue"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigConstantValue(string comparableValue, string displayValue)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(comparableValue);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
|
||||
if (comparableValue.Length > 0 &&
|
||||
string.IsNullOrWhiteSpace(comparableValue))
|
||||
{
|
||||
throw new ArgumentException("The value cannot be composed entirely of whitespace.", nameof(comparableValue));
|
||||
}
|
||||
|
||||
ComparableValue = comparableValue;
|
||||
DisplayValue = displayValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于运行时比较的稳定键。
|
||||
/// </summary>
|
||||
public string ComparableValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断输出的原始 JSON 常量文本。
|
||||
/// </summary>
|
||||
public string DisplayValue { get; }
|
||||
}
|
||||
55
GFramework.Game/Config/YamlConfigNumericConstraints.cs
Normal file
55
GFramework.Game/Config/YamlConfigNumericConstraints.cs
Normal file
@ -0,0 +1,55 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示标量节点上声明的数值范围与步进约束。
|
||||
/// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigNumericConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数值约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minimum">最小值约束。</param>
|
||||
/// <param name="maximum">最大值约束。</param>
|
||||
/// <param name="exclusiveMinimum">开区间最小值约束。</param>
|
||||
/// <param name="exclusiveMaximum">开区间最大值约束。</param>
|
||||
/// <param name="multipleOf">数值步进约束。</param>
|
||||
public YamlConfigNumericConstraints(
|
||||
double? minimum,
|
||||
double? maximum,
|
||||
double? exclusiveMinimum,
|
||||
double? exclusiveMaximum,
|
||||
double? multipleOf)
|
||||
{
|
||||
Minimum = minimum;
|
||||
Maximum = maximum;
|
||||
ExclusiveMinimum = exclusiveMinimum;
|
||||
ExclusiveMaximum = exclusiveMaximum;
|
||||
MultipleOf = multipleOf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小值约束。
|
||||
/// </summary>
|
||||
public double? Minimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大值约束。
|
||||
/// </summary>
|
||||
public double? Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最小值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMinimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最大值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMaximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数值步进约束。
|
||||
/// </summary>
|
||||
public double? MultipleOf { get; }
|
||||
}
|
||||
86
GFramework.Game/Config/YamlConfigObjectConstraints.cs
Normal file
86
GFramework.Game/Config/YamlConfigObjectConstraints.cs
Normal file
@ -0,0 +1,86 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个对象节点上声明的属性数量约束、字段依赖约束、条件子 schema 与组合约束。
|
||||
/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigObjectConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化对象约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minProperties">最小属性数量约束。</param>
|
||||
/// <param name="maxProperties">最大属性数量约束。</param>
|
||||
/// <param name="dependentRequired">对象内字段依赖约束。</param>
|
||||
/// <param name="dependentSchemas">对象内条件 schema 约束。</param>
|
||||
/// <param name="allOfSchemas">对象内组合 schema 约束。</param>
|
||||
/// <param name="conditionalSchemas">对象内条件分支约束。</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="minProperties"/> 或 <paramref name="maxProperties"/> 为负数时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="minProperties"/> 大于 <paramref name="maxProperties"/> 时抛出。</exception>
|
||||
public YamlConfigObjectConstraints(
|
||||
int? minProperties,
|
||||
int? maxProperties,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>>? dependentRequired,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? dependentSchemas,
|
||||
IReadOnlyList<YamlConfigSchemaNode>? allOfSchemas,
|
||||
YamlConfigConditionalSchemas? conditionalSchemas)
|
||||
{
|
||||
if (minProperties is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minProperties), minProperties, "minProperties 不能为负数。");
|
||||
}
|
||||
|
||||
if (maxProperties is < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxProperties), maxProperties, "maxProperties 不能为负数。");
|
||||
}
|
||||
|
||||
if (minProperties.HasValue &&
|
||||
maxProperties.HasValue &&
|
||||
minProperties.Value > maxProperties.Value)
|
||||
{
|
||||
throw new ArgumentException("minProperties 不能大于 maxProperties。", nameof(minProperties));
|
||||
}
|
||||
|
||||
MinProperties = minProperties;
|
||||
MaxProperties = maxProperties;
|
||||
DependentRequired = dependentRequired;
|
||||
DependentSchemas = dependentSchemas;
|
||||
AllOfSchemas = allOfSchemas;
|
||||
ConditionalSchemas = conditionalSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小属性数量约束。
|
||||
/// </summary>
|
||||
public int? MinProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大属性数量约束。
|
||||
/// </summary>
|
||||
public int? MaxProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内字段依赖约束。
|
||||
/// 键表示“触发字段”,值表示“触发字段出现后还必须存在的同级字段集合”。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>>? DependentRequired { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内条件 schema 约束。
|
||||
/// 键表示“触发字段”,值表示“触发字段出现后当前对象还必须满足的额外 schema 子树”。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? DependentSchemas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内 <c>allOf</c> 组合约束。
|
||||
/// 每个条目都表示“当前对象还必须额外满足的 focused constraint block”。
|
||||
/// </summary>
|
||||
public IReadOnlyList<YamlConfigSchemaNode>? AllOfSchemas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 该模型会先用 <c>if</c> 试匹配当前对象,再只对命中的分支叠加 focused constraint block。
|
||||
/// </summary>
|
||||
public YamlConfigConditionalSchemas? ConditionalSchemas { get; }
|
||||
}
|
||||
74
GFramework.Game/Config/YamlConfigReferenceUsage.cs
Normal file
74
GFramework.Game/Config/YamlConfigReferenceUsage.cs
Normal file
@ -0,0 +1,74 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示单个 YAML 文件中提取出的跨表引用。
|
||||
/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigReferenceUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个跨表引用使用记录。
|
||||
/// </summary>
|
||||
/// <param name="yamlPath">源 YAML 文件路径。</param>
|
||||
/// <param name="schemaPath">定义该引用的 schema 文件路径。</param>
|
||||
/// <param name="propertyPath">声明引用的字段路径。</param>
|
||||
/// <param name="rawValue">YAML 中的原始标量值。</param>
|
||||
/// <param name="referencedTableName">目标配置表名称。</param>
|
||||
/// <param name="valueType">引用值的 schema 标量类型。</param>
|
||||
public YamlConfigReferenceUsage(
|
||||
string yamlPath,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string rawValue,
|
||||
string referencedTableName,
|
||||
YamlConfigSchemaPropertyType valueType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(yamlPath);
|
||||
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||
ArgumentNullException.ThrowIfNull(propertyPath);
|
||||
ArgumentNullException.ThrowIfNull(rawValue);
|
||||
ArgumentNullException.ThrowIfNull(referencedTableName);
|
||||
|
||||
YamlPath = yamlPath;
|
||||
SchemaPath = schemaPath;
|
||||
PropertyPath = propertyPath;
|
||||
RawValue = rawValue;
|
||||
ReferencedTableName = referencedTableName;
|
||||
ValueType = valueType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取源 YAML 文件路径。
|
||||
/// </summary>
|
||||
public string YamlPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取定义该引用的 schema 文件路径。
|
||||
/// </summary>
|
||||
public string SchemaPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取声明引用的字段路径。
|
||||
/// </summary>
|
||||
public string PropertyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 YAML 中的原始标量值。
|
||||
/// </summary>
|
||||
public string RawValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标配置表名称。
|
||||
/// </summary>
|
||||
public string ReferencedTableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取引用值的 schema 标量类型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType ValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取便于诊断显示的字段路径。
|
||||
/// </summary>
|
||||
public string DisplayPath => PropertyPath;
|
||||
}
|
||||
31
GFramework.Game/Config/YamlConfigScalarConstraints.cs
Normal file
31
GFramework.Game/Config/YamlConfigScalarConstraints.cs
Normal file
@ -0,0 +1,31 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 聚合一个标量节点上声明的数值约束与字符串约束。
|
||||
/// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigScalarConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化标量约束模型。
|
||||
/// </summary>
|
||||
/// <param name="numericConstraints">数值约束分组。</param>
|
||||
/// <param name="stringConstraints">字符串约束分组。</param>
|
||||
public YamlConfigScalarConstraints(
|
||||
YamlConfigNumericConstraints? numericConstraints,
|
||||
YamlConfigStringConstraints? stringConstraints)
|
||||
{
|
||||
NumericConstraints = numericConstraints;
|
||||
StringConstraints = stringConstraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数值约束分组。
|
||||
/// </summary>
|
||||
public YamlConfigNumericConstraints? NumericConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串约束分组。
|
||||
/// </summary>
|
||||
public YamlConfigStringConstraints? StringConstraints { get; }
|
||||
}
|
||||
45
GFramework.Game/Config/YamlConfigSchema.cs
Normal file
45
GFramework.Game/Config/YamlConfigSchema.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示已解析并可用于运行时校验的 JSON Schema。
|
||||
/// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个可用于运行时校验的 schema 模型。
|
||||
/// </summary>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="rootNode">根节点模型。</param>
|
||||
/// <param name="referencedTableNames">Schema 声明的目标引用表名称集合。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="schemaPath"/>、<paramref name="rootNode"/> 或 <paramref name="referencedTableNames"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
public YamlConfigSchema(
|
||||
string schemaPath,
|
||||
YamlConfigSchemaNode rootNode,
|
||||
IReadOnlyCollection<string> referencedTableNames)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||
ArgumentNullException.ThrowIfNull(rootNode);
|
||||
ArgumentNullException.ThrowIfNull(referencedTableNames);
|
||||
|
||||
SchemaPath = schemaPath;
|
||||
RootNode = rootNode;
|
||||
ReferencedTableNames = [.. referencedTableNames];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 文件路径。
|
||||
/// </summary>
|
||||
public string SchemaPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取根节点模型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode RootNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 声明的目标引用表名称集合。
|
||||
/// 该信息用于热重载时推导受影响的依赖表闭包。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> ReferencedTableNames { get; }
|
||||
}
|
||||
321
GFramework.Game/Config/YamlConfigSchemaNode.cs
Normal file
321
GFramework.Game/Config/YamlConfigSchemaNode.cs
Normal file
@ -0,0 +1,321 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示单个 schema 节点的最小运行时描述。
|
||||
/// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigSchemaNode
|
||||
{
|
||||
private readonly NodeChildren _children;
|
||||
private readonly NodeValidation _validation;
|
||||
|
||||
private YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType nodeType,
|
||||
NodeChildren children,
|
||||
NodeValidation validation,
|
||||
string schemaPathHint)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(children);
|
||||
ArgumentNullException.ThrowIfNull(validation);
|
||||
ArgumentNullException.ThrowIfNull(schemaPathHint);
|
||||
|
||||
_children = children;
|
||||
_validation = validation;
|
||||
NodeType = nodeType;
|
||||
Properties = children.Properties;
|
||||
RequiredProperties = children.RequiredProperties;
|
||||
ItemNode = children.ItemNode;
|
||||
ReferenceTableName = validation.ReferenceTableName;
|
||||
AllowedValues = validation.AllowedValues;
|
||||
Constraints = validation.Constraints;
|
||||
ArrayConstraints = validation.ArrayConstraints;
|
||||
ObjectConstraints = validation.ObjectConstraints;
|
||||
ConstantValue = validation.ConstantValue;
|
||||
NegatedSchemaNode = validation.NegatedSchemaNode;
|
||||
SchemaPathHint = schemaPathHint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点类型。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaPropertyType NodeType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象属性集合;非对象节点时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象必填属性集合;非对象节点时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string>? RequiredProperties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素节点;非数组节点时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? ItemNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标引用表名称;未声明跨表引用时返回空。
|
||||
/// </summary>
|
||||
public string? ReferenceTableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点允许值集合;未声明 <c>enum</c> 时返回空。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取标量范围与长度约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigScalarConstraints? Constraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象属性数量约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigObjectConstraints? ObjectConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素数量约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigArrayConstraints? ArrayConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点常量约束;未声明 <c>const</c> 时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigConstantValue? ConstantValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取节点声明的 <c>not</c> 子 schema;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断显示的 schema 路径提示。
|
||||
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
|
||||
/// </summary>
|
||||
public string SchemaPathHint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建对象节点描述。
|
||||
/// </summary>
|
||||
/// <param name="properties">对象属性集合。</param>
|
||||
/// <param name="requiredProperties">对象必填属性集合。</param>
|
||||
/// <param name="objectConstraints">对象属性数量约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>对象节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateObject(
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
|
||||
IReadOnlyCollection<string>? requiredProperties,
|
||||
YamlConfigObjectConstraints? objectConstraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType.Object,
|
||||
new NodeChildren(properties, requiredProperties, itemNode: null),
|
||||
new NodeValidation(
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
arrayConstraints: null,
|
||||
objectConstraints,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建数组节点描述。
|
||||
/// </summary>
|
||||
/// <param name="itemNode">数组元素节点。</param>
|
||||
/// <param name="allowedValues">数组节点允许值集合。</param>
|
||||
/// <param name="arrayConstraints">数组元素数量约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>数组节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateArray(
|
||||
YamlConfigSchemaNode itemNode,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigArrayConstraints? arrayConstraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType.Array,
|
||||
new NodeChildren(properties: null, requiredProperties: null, itemNode),
|
||||
new NodeValidation(
|
||||
referenceTableName: null,
|
||||
allowedValues,
|
||||
constraints: null,
|
||||
arrayConstraints,
|
||||
objectConstraints: null,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建标量节点描述。
|
||||
/// </summary>
|
||||
/// <param name="nodeType">标量节点类型。</param>
|
||||
/// <param name="referenceTableName">目标引用表名称。</param>
|
||||
/// <param name="allowedValues">标量允许值集合。</param>
|
||||
/// <param name="constraints">标量范围与长度约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
/// <returns>标量节点模型。</returns>
|
||||
public static YamlConfigSchemaNode CreateScalar(
|
||||
YamlConfigSchemaPropertyType nodeType,
|
||||
string? referenceTableName,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigScalarConstraints? constraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
nodeType,
|
||||
NodeChildren.None,
|
||||
new NodeValidation(
|
||||
referenceTableName,
|
||||
allowedValues,
|
||||
constraints,
|
||||
arrayConstraints: null,
|
||||
objectConstraints: null,
|
||||
constantValue: null,
|
||||
negatedSchemaNode: null),
|
||||
schemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换引用表名称的新节点。
|
||||
/// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。
|
||||
/// </summary>
|
||||
/// <param name="referenceTableName">新的目标引用表名称。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithReferenceTable(string referenceTableName)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithReferenceTable(referenceTableName),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换 <c>enum</c> 允许值集合的新节点。
|
||||
/// </summary>
|
||||
/// <param name="allowedValues">新的允许值集合。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithAllowedValues(allowedValues),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换常量约束的新节点。
|
||||
/// </summary>
|
||||
/// <param name="constantValue">新的常量约束。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithConstantValue(constantValue),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于当前节点复制一个只替换 <c>not</c> 子 schema 的新节点。
|
||||
/// </summary>
|
||||
/// <param name="negatedSchemaNode">新的 negated schema。</param>
|
||||
/// <returns>复制后的节点。</returns>
|
||||
public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
return new YamlConfigSchemaNode(
|
||||
NodeType,
|
||||
_children,
|
||||
_validation.WithNegatedSchemaNode(negatedSchemaNode),
|
||||
SchemaPathHint);
|
||||
}
|
||||
|
||||
private sealed class NodeChildren
|
||||
{
|
||||
public NodeChildren(
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode>? properties,
|
||||
IReadOnlyCollection<string>? requiredProperties,
|
||||
YamlConfigSchemaNode? itemNode)
|
||||
{
|
||||
Properties = properties;
|
||||
RequiredProperties = requiredProperties;
|
||||
ItemNode = itemNode;
|
||||
}
|
||||
|
||||
public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null);
|
||||
|
||||
public IReadOnlyDictionary<string, YamlConfigSchemaNode>? Properties { get; }
|
||||
|
||||
public IReadOnlyCollection<string>? RequiredProperties { get; }
|
||||
|
||||
public YamlConfigSchemaNode? ItemNode { get; }
|
||||
}
|
||||
|
||||
private sealed class NodeValidation
|
||||
{
|
||||
public NodeValidation(
|
||||
string? referenceTableName,
|
||||
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||
YamlConfigScalarConstraints? constraints,
|
||||
YamlConfigArrayConstraints? arrayConstraints,
|
||||
YamlConfigObjectConstraints? objectConstraints,
|
||||
YamlConfigConstantValue? constantValue,
|
||||
YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
ReferenceTableName = referenceTableName;
|
||||
AllowedValues = allowedValues;
|
||||
Constraints = constraints;
|
||||
ArrayConstraints = arrayConstraints;
|
||||
ObjectConstraints = objectConstraints;
|
||||
ConstantValue = constantValue;
|
||||
NegatedSchemaNode = negatedSchemaNode;
|
||||
}
|
||||
|
||||
public string? ReferenceTableName { get; }
|
||||
|
||||
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||
|
||||
public YamlConfigScalarConstraints? Constraints { get; }
|
||||
|
||||
public YamlConfigArrayConstraints? ArrayConstraints { get; }
|
||||
|
||||
public YamlConfigObjectConstraints? ObjectConstraints { get; }
|
||||
|
||||
public YamlConfigConstantValue? ConstantValue { get; }
|
||||
|
||||
public YamlConfigSchemaNode? NegatedSchemaNode { get; }
|
||||
|
||||
public NodeValidation WithReferenceTable(string referenceTableName)
|
||||
{
|
||||
return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, constantValue, NegatedSchemaNode);
|
||||
}
|
||||
|
||||
public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode)
|
||||
{
|
||||
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||
ObjectConstraints, ConstantValue, negatedSchemaNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
Normal file
40
GFramework.Game/Config/YamlConfigSchemaPropertyType.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前运行时 schema 校验器支持的属性类型。
|
||||
/// </summary>
|
||||
[GenerateEnumExtensions]
|
||||
internal enum YamlConfigSchemaPropertyType
|
||||
{
|
||||
/// <summary>
|
||||
/// 对象类型。
|
||||
/// </summary>
|
||||
Object,
|
||||
|
||||
/// <summary>
|
||||
/// 整数类型。
|
||||
/// </summary>
|
||||
Integer,
|
||||
|
||||
/// <summary>
|
||||
/// 数值类型。
|
||||
/// </summary>
|
||||
Number,
|
||||
|
||||
/// <summary>
|
||||
/// 布尔类型。
|
||||
/// </summary>
|
||||
Boolean,
|
||||
|
||||
/// <summary>
|
||||
/// 字符串类型。
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// 数组类型。
|
||||
/// </summary>
|
||||
Array
|
||||
}
|
||||
@ -104,67 +104,12 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var dependentRequired = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
|
||||
foreach (var dependency in dependentRequiredElement.EnumerateObject())
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargets = new List<string>();
|
||||
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
|
||||
{
|
||||
if (dependencyTarget.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargetName = dependencyTarget.GetString();
|
||||
if (string.IsNullOrWhiteSpace(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (!properties.ContainsKey(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (seenDependencyTargets.Add(dependencyTargetName))
|
||||
{
|
||||
dependencyTargets.Add(dependencyTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
var dependencyTargets = ParseDependentRequiredConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
dependency,
|
||||
properties);
|
||||
if (dependencyTargets.Count > 0)
|
||||
{
|
||||
dependentRequired[dependency.Name] = dependencyTargets;
|
||||
@ -176,6 +121,116 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: dependentRequired;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个 <c>dependentRequired</c> 触发字段的依赖目标列表。
|
||||
/// 触发字段和目标字段必须都来自父对象已声明属性;重复目标会被去重以保持运行时约束稳定。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependency">当前触发字段声明。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>去重后的依赖目标列表。</returns>
|
||||
private static IReadOnlyList<string> ParseDependentRequiredConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonProperty dependency,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargets = new List<string>();
|
||||
var seenDependencyTargets = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
|
||||
{
|
||||
var dependencyTargetName = ParseDependentRequiredTargetName(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
dependency.Name,
|
||||
dependencyTarget,
|
||||
properties);
|
||||
if (seenDependencyTargets.Add(dependencyTargetName))
|
||||
{
|
||||
dependencyTargets.Add(dependencyTargetName);
|
||||
}
|
||||
}
|
||||
|
||||
return dependencyTargets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取并校验 <c>dependentRequired</c> 的单个目标字段名。
|
||||
/// 目标必须是非空字符串并已在同一个对象 schema 中声明,避免依赖关系指向不可满足字段。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependencyName">当前触发字段名称。</param>
|
||||
/// <param name="dependencyTarget">当前依赖目标节点。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>已校验的依赖目标字段名。</returns>
|
||||
private static string ParseDependentRequiredTargetName(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string dependencyName,
|
||||
JsonElement dependencyTarget,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (dependencyTarget.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencyTargetName = dependencyTarget.GetString();
|
||||
if (string.IsNullOrWhiteSpace(dependencyTargetName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(dependencyTargetName))
|
||||
{
|
||||
return dependencyTargetName;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 <c>dependentSchemas</c> 条件 schema。
|
||||
/// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释,
|
||||
@ -212,43 +267,12 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var dependentSchemas = new Dictionary<string, YamlConfigSchemaNode>(StringComparer.Ordinal);
|
||||
foreach (var dependency in dependentSchemasElement.EnumerateObject())
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
|
||||
var dependencySchemaNode = ParseNode(
|
||||
dependentSchemas[dependency.Name] = ParseDependentSchemaConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
dependencySchemaPath,
|
||||
dependency.Value);
|
||||
if (dependencySchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(dependencySchemaPath));
|
||||
}
|
||||
|
||||
dependentSchemas[dependency.Name] = dependencySchemaNode;
|
||||
propertyPath,
|
||||
dependency,
|
||||
properties);
|
||||
}
|
||||
|
||||
return dependentSchemas.Count == 0
|
||||
@ -256,6 +280,62 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: dependentSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个 <c>dependentSchemas</c> 触发字段关联的 object-typed schema。
|
||||
/// 触发字段必须属于当前对象,关联 schema 继续通过通用节点解析流程获得完整约束模型。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="dependency">当前触发字段声明。</param>
|
||||
/// <param name="properties">当前对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的 object-typed 条件 schema。</returns>
|
||||
private static YamlConfigSchemaNode ParseDependentSchemaConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonProperty dependency,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!properties.ContainsKey(dependency.Name))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (dependency.Value.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}");
|
||||
var dependencySchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
dependencySchemaPath,
|
||||
dependency.Value);
|
||||
if (dependencySchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
return dependencySchemaNode;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(dependencySchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 <c>allOf</c> 组合约束。
|
||||
/// 当前实现仅接受 object-typed 内联 schema,并把每个条目当成 focused constraint block
|
||||
@ -293,40 +373,13 @@ internal static partial class YamlConfigSchemaValidator
|
||||
var allOfIndex = 0;
|
||||
foreach (var allOfSchemaElement in allOfElement.EnumerateArray())
|
||||
{
|
||||
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
|
||||
ValidateInlineObjectSchemaTargetsAgainstParentObject(
|
||||
var allOfSchemaNode = ParseAllOfSchemaConstraint(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
allOfSchemaPath,
|
||||
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'",
|
||||
allOfSchemaElement,
|
||||
allOfIndex,
|
||||
properties);
|
||||
var allOfSchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
allOfSchemaPath,
|
||||
allOfSchemaElement);
|
||||
if (allOfSchemaNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(allOfSchemaPath));
|
||||
}
|
||||
|
||||
allOfSchemas.Add(allOfSchemaNode);
|
||||
allOfIndex++;
|
||||
}
|
||||
@ -336,6 +389,64 @@ internal static partial class YamlConfigSchemaValidator
|
||||
: allOfSchemas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 <c>allOf</c> 中的单个 object-focused schema 条目。
|
||||
/// 每个条目只允许约束父对象已声明的字段,并且必须保持 object-typed 语义。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="allOfSchemaElement">当前 allOf 条目。</param>
|
||||
/// <param name="allOfIndex">当前 allOf 条目的零基索引。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的 object-typed schema。</returns>
|
||||
private static YamlConfigSchemaNode ParseAllOfSchemaConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement allOfSchemaElement,
|
||||
int allOfIndex,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (allOfSchemaElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var allOfEntryLabel = $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'";
|
||||
var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]");
|
||||
ValidateInlineObjectSchemaTargetsAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
allOfSchemaPath,
|
||||
allOfEntryLabel,
|
||||
allOfSchemaElement,
|
||||
properties);
|
||||
|
||||
var allOfSchemaNode = ParseNode(
|
||||
tableName,
|
||||
schemaPath,
|
||||
allOfSchemaPath,
|
||||
allOfSchemaElement);
|
||||
if (allOfSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object)
|
||||
{
|
||||
return allOfSchemaNode;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{allOfEntryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(allOfSchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析对象节点声明的 object-focused <c>if</c> / <c>then</c> / <c>else</c> 条件约束。
|
||||
/// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义,
|
||||
@ -362,25 +473,7 @@ internal static partial class YamlConfigSchemaValidator
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasIf)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (!hasThen && !hasElse)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
ValidateConditionalSchemaKeywordPresence(tableName, schemaPath, propertyPath, hasIf, hasThen, hasElse);
|
||||
|
||||
var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if");
|
||||
var ifSchemaNode = ParseConditionalObjectSchema(
|
||||
@ -391,31 +484,100 @@ internal static partial class YamlConfigSchemaValidator
|
||||
"if",
|
||||
ifElement,
|
||||
properties);
|
||||
|
||||
var thenSchemaNode = hasThen
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, "then"),
|
||||
"then",
|
||||
thenElement,
|
||||
properties)
|
||||
: null;
|
||||
var elseSchemaNode = hasElse
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, "else"),
|
||||
"else",
|
||||
elseElement,
|
||||
properties)
|
||||
: null;
|
||||
var thenSchemaNode = ParseOptionalConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
"then",
|
||||
hasThen,
|
||||
thenElement,
|
||||
properties);
|
||||
var elseSchemaNode = ParseOptionalConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
"else",
|
||||
hasElse,
|
||||
elseElement,
|
||||
properties);
|
||||
|
||||
return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 条件关键字的组合关系。
|
||||
/// <c>then</c> 与 <c>else</c> 只能跟随 <c>if</c>,而单独的 <c>if</c> 没有可执行分支。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="hasIf">是否声明 if。</param>
|
||||
/// <param name="hasThen">是否声明 then。</param>
|
||||
/// <param name="hasElse">是否声明 else。</param>
|
||||
private static void ValidateConditionalSchemaKeywordPresence(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
bool hasIf,
|
||||
bool hasThen,
|
||||
bool hasElse)
|
||||
{
|
||||
if (!hasIf)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (hasThen || hasElse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析可选的 <c>then</c> 或 <c>else</c> 条件分支。
|
||||
/// 未声明的分支保留为空,声明的分支必须通过 object-focused schema 校验。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">对象字段路径。</param>
|
||||
/// <param name="keywordName">条件关键字名称。</param>
|
||||
/// <param name="hasKeyword">是否声明该条件关键字。</param>
|
||||
/// <param name="keywordElement">条件关键字对应的 schema 节点。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
/// <returns>解析后的条件分支;未声明时返回空。</returns>
|
||||
private static YamlConfigSchemaNode? ParseOptionalConditionalObjectSchema(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string keywordName,
|
||||
bool hasKeyword,
|
||||
JsonElement keywordElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
return hasKeyword
|
||||
? ParseConditionalObjectSchema(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
BuildNestedSchemaPath(propertyPath, keywordName),
|
||||
keywordName,
|
||||
keywordElement,
|
||||
properties)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个条件分支的 object-focused 内联 schema。
|
||||
/// </summary>
|
||||
@ -494,34 +656,96 @@ internal static partial class YamlConfigSchemaValidator
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
|
||||
ValidateInlineObjectSchemaPropertiesAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
inlineSchemaElement,
|
||||
properties);
|
||||
|
||||
ValidateInlineRequiredPropertiesAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
inlineSchemaElement,
|
||||
properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的 <c>properties</c> 只引用父对象字段。
|
||||
/// focused block 不负责声明新字段,所以任何父对象未声明字段都会在 schema 加载时被拒绝。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="inlineSchemaElement">当前内联 schema。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineObjectSchemaPropertiesAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement))
|
||||
{
|
||||
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
foreach (var property in inlinePropertiesElement.EnumerateObject())
|
||||
{
|
||||
if (properties.ContainsKey(property.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inlinePropertiesElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
foreach (var property in inlinePropertiesElement.EnumerateObject())
|
||||
{
|
||||
if (properties.ContainsKey(property.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的 <c>required</c> 只引用父对象字段。
|
||||
/// 该校验在加载期暴露不可满足的条件块,而不是等到运行时才发现无效字段名。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="inlineSchemaElement">当前内联 schema。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineRequiredPropertiesAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement inlineSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement))
|
||||
{
|
||||
return;
|
||||
@ -539,39 +763,69 @@ internal static partial class YamlConfigSchemaValidator
|
||||
|
||||
foreach (var requiredProperty in inlineRequiredElement.EnumerateArray())
|
||||
{
|
||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
var requiredPropertyName = requiredProperty.GetString();
|
||||
if (string.IsNullOrWhiteSpace(requiredPropertyName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(requiredPropertyName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ValidateInlineRequiredPropertyAgainstParentObject(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
inlineSchemaPath,
|
||||
entryLabel,
|
||||
requiredProperty,
|
||||
properties);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 object-focused 内联 schema 的单个 <c>required</c> 字段名。
|
||||
/// 字段名必须是非空字符串并且属于父对象声明范围,保持条件块与父对象形状一致。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">父对象路径。</param>
|
||||
/// <param name="inlineSchemaPath">当前内联 schema 路径。</param>
|
||||
/// <param name="entryLabel">用于诊断文本的条目标签。</param>
|
||||
/// <param name="requiredProperty">当前 required 条目。</param>
|
||||
/// <param name="properties">父对象已声明的属性集合。</param>
|
||||
private static void ValidateInlineRequiredPropertyAgainstParentObject(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
string inlineSchemaPath,
|
||||
string entryLabel,
|
||||
JsonElement requiredProperty,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||
{
|
||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
var requiredPropertyName = requiredProperty.GetString();
|
||||
if (string.IsNullOrWhiteSpace(requiredPropertyName))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
if (properties.ContainsKey(requiredPropertyName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(inlineSchemaPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
64
GFramework.Game/Config/YamlConfigStringConstraints.cs
Normal file
64
GFramework.Game/Config/YamlConfigStringConstraints.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示标量节点上声明的字符串长度、模式与 format 约束。
|
||||
/// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存,
|
||||
/// 保证诊断内容与运行时匹配逻辑保持一致。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigStringConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化字符串约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minLength">最小长度约束。</param>
|
||||
/// <param name="maxLength">最大长度约束。</param>
|
||||
/// <param name="pattern">正则模式约束原文。</param>
|
||||
/// <param name="patternRegex">已编译的正则表达式。</param>
|
||||
/// <param name="formatConstraint">字符串 format 约束。</param>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="pattern"/> 与 <paramref name="patternRegex"/> 未成对出现时抛出。</exception>
|
||||
public YamlConfigStringConstraints(
|
||||
int? minLength,
|
||||
int? maxLength,
|
||||
string? pattern,
|
||||
Regex? patternRegex,
|
||||
YamlConfigStringFormatConstraint? formatConstraint)
|
||||
{
|
||||
if ((pattern is null) != (patternRegex is null))
|
||||
{
|
||||
throw new ArgumentException("pattern 与 patternRegex 必须同时为空或同时提供。", nameof(pattern));
|
||||
}
|
||||
|
||||
MinLength = minLength;
|
||||
MaxLength = maxLength;
|
||||
Pattern = pattern;
|
||||
PatternRegex = patternRegex;
|
||||
FormatConstraint = formatConstraint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小长度约束。
|
||||
/// </summary>
|
||||
public int? MinLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大长度约束。
|
||||
/// </summary>
|
||||
public int? MaxLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取正则模式约束原文。
|
||||
/// </summary>
|
||||
public string? Pattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取已编译的正则表达式。
|
||||
/// </summary>
|
||||
public Regex? PatternRegex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串 format 约束。
|
||||
/// </summary>
|
||||
public YamlConfigStringFormatConstraint? FormatConstraint { get; }
|
||||
}
|
||||
35
GFramework.Game/Config/YamlConfigStringFormatConstraint.cs
Normal file
35
GFramework.Game/Config/YamlConfigStringFormatConstraint.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个已归一化的字符串 format 约束。
|
||||
/// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigStringFormatConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化字符串 format 约束模型。
|
||||
/// </summary>
|
||||
/// <param name="schemaName">schema 中声明的 format 名称。</param>
|
||||
/// <param name="kind">归一化后的共享 format 枚举。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="schemaName"/> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">当 <paramref name="schemaName"/> 为空或仅包含空白字符时抛出。</exception>
|
||||
public YamlConfigStringFormatConstraint(
|
||||
string schemaName,
|
||||
YamlConfigStringFormatKind kind)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(schemaName);
|
||||
|
||||
SchemaName = schemaName;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 schema 中声明的 format 名称。
|
||||
/// </summary>
|
||||
public string SchemaName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取归一化后的共享 format 枚举。
|
||||
/// </summary>
|
||||
public YamlConfigStringFormatKind Kind { get; }
|
||||
}
|
||||
45
GFramework.Game/Config/YamlConfigStringFormatKind.cs
Normal file
45
GFramework.Game/Config/YamlConfigStringFormatKind.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
|
||||
/// <summary>
|
||||
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
|
||||
/// </summary>
|
||||
[GenerateEnumExtensions]
|
||||
internal enum YamlConfigStringFormatKind
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示 <c>yyyy-MM-dd</c> 形式的日期。
|
||||
/// </summary>
|
||||
Date,
|
||||
|
||||
/// <summary>
|
||||
/// 表示带显式时区偏移的 RFC 3339 日期时间。
|
||||
/// </summary>
|
||||
DateTime,
|
||||
|
||||
/// <summary>
|
||||
/// 表示 day-time duration 形式的持续时间。
|
||||
/// </summary>
|
||||
Duration,
|
||||
|
||||
/// <summary>
|
||||
/// 表示基础电子邮件地址格式。
|
||||
/// </summary>
|
||||
Email,
|
||||
|
||||
/// <summary>
|
||||
/// 表示带显式时区偏移的 RFC 3339 时间。
|
||||
/// </summary>
|
||||
Time,
|
||||
|
||||
/// <summary>
|
||||
/// 表示绝对 URI。
|
||||
/// </summary>
|
||||
Uri,
|
||||
|
||||
/// <summary>
|
||||
/// 表示连字符分隔的 UUID 文本。
|
||||
/// </summary>
|
||||
Uuid
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Core.SourceGenerators.Abstractions\GFramework.Core.SourceGenerators.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
@ -12,11 +12,6 @@ help the current worktree land on the right recovery documents without scanning
|
||||
|
||||
## Active Topics
|
||||
|
||||
- `analyzer-warning-reduction`
|
||||
- Purpose: track the analyzer warning reduction branch, including the current recovery point, remaining warning
|
||||
hotspots, and the next safe warning-reduction slice.
|
||||
- Tracking: `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
|
||||
- Trace: `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
|
||||
- `ai-plan-governance`
|
||||
- Purpose: govern the `ai-plan/` directory model, startup index, and archive policy.
|
||||
- Tracking: `ai-plan/public/ai-plan-governance/todos/ai-plan-governance-tracking.md`
|
||||
@ -50,9 +45,6 @@ help the current worktree land on the right recovery documents without scanning
|
||||
|
||||
## Worktree To Active Topic Map
|
||||
|
||||
- Branch: `fix/analyzer-warning-reduction-batch`
|
||||
- Worktree hint: `GFramework-analyzer`
|
||||
- Priority 1: `analyzer-warning-reduction`
|
||||
- Branch: `feat/ai-first-config`
|
||||
- Worktree hint: `GFramework-Ai-First-Config`
|
||||
- Priority 1: `ai-first-config-system`
|
||||
@ -75,6 +67,9 @@ help the current worktree land on the right recovery documents without scanning
|
||||
- Priority 1: `documentation-full-coverage-governance`
|
||||
## Archived Topics
|
||||
|
||||
- `analyzer-warning-reduction`
|
||||
- Archive root: `ai-plan/public/archive/analyzer-warning-reduction/`
|
||||
- Note: 长期 warning-reduction 分支已收尾;PR #301 的最终 review follow-up 已本地闭环,后续仅作为历史恢复材料保留。
|
||||
- `cqrs-cache-docs-hardening`
|
||||
- Archive root: `ai-plan/public/archive/cqrs-cache-docs-hardening/`
|
||||
- Note: archived topics stay outside the default `boot` context until a user explicitly requests historical review.
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
# Analyzer Warning Reduction 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-092`
|
||||
- 当前阶段:`Phase 92`
|
||||
- 当前焦点:
|
||||
- `2026-04-28` 复核 `PR #300` 最新 open threads:代码类线程已与当前工作树对齐,仅剩 `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md` 的文件计数与验证口径漂移仍然成立
|
||||
- 已将 tracking 文档修正为与 `6cc87a9...HEAD` 的实际变更规模一致,并与 trace 中记录的 `dotnet build`、定向 `dotnet test`、`git diff --check` 验证口径保持一致
|
||||
- `dotnet format --verify-no-changes` 的 `GFramework.Core.Tests` 既有 `FINALNEWLINE`、`CHARSET`、`WHITESPACE` 基线仍保持独立,不与当前 `ai-plan` 同步修复混提
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前 `origin/main` 基线提交为 `6cc87a9`(`2026-04-27T20:28:50+08:00`)。
|
||||
- 当前直接验证结果:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 最新结果:成功;`67` 通过、`0` 失败
|
||||
- 当前批次摘要:
|
||||
- 当前分支相对 `6cc87a9...HEAD` 包含 `18` 个已修改文件与 `38` 个新增文件(合计 `56` 个变更文件),分别位于 `GFramework.Core.Tests`、`GFramework.Cqrs.Tests`、`GFramework.Core`、`.agents/skills/gframework-pr-review/` 与 `ai-plan/public/analyzer-warning-reduction`
|
||||
- 本轮没有触碰 `Mediator/*`、`YamlConfigSchemaValidator*` 或 `GFramework.Core.Tests` 的整项目格式基线波次
|
||||
|
||||
## 当前风险
|
||||
|
||||
- GitHub PR 上的 open threads 可能仍显示为未关闭,因为当前只同步了 `ai-plan` 文档,尚未推送新的 head 供审查机器人重新折叠线程。
|
||||
- 缓解措施:推送本次 `ai-plan` 同步提交后重新执行 `$gframework-pr-review`,以最新 head 再核对 thread 状态。
|
||||
- `dotnet format GFramework.Core.Tests/GFramework.Core.Tests.csproj --verify-no-changes` 当前会命中项目内大量历史格式诊断。
|
||||
- 缓解措施:本轮只记录为现存基线,不把 `PR #300` 的 review follow-up 扩展成整项目格式清理。
|
||||
- `GFramework.Game/Config/YamlConfigSchemaValidator*` 仍然是仓库根 warning 热点,但与本轮 review 修复无交集。
|
||||
- 缓解措施:继续保持为独立高耦合波次。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 当前轮次归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 历史跟踪归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- 历史 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 权威验证结果统一维护在“当前活跃事实”。
|
||||
- `GFramework.Core.Tests` 的当前受影响项目 Release 构建已清零,并通过对应定向测试回归。
|
||||
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
||||
- warning reduction 的仓库级真值以同轮 `dotnet build`、定向 `dotnet test` 与 `git diff --check` 为准,并与 trace 中的验证里程碑保持一致。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 提交本轮 `PR #300` nitpick follow-up、技能规则更新与 `ai-plan` 同步。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认 `ai-plan` 相关 thread 是否随最新 head 自动收口。
|
||||
3. 若要清理 `dotnet format` 基线,另开 `GFramework.Core.Tests` 格式治理切片,不与当前 PR review 修复混提。
|
||||
@ -1,67 +0,0 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-28 — RP-092
|
||||
|
||||
### 阶段:复核 `PR #300` 的 open threads,并只修正当前分支仍然成立的 `ai-plan` 漂移
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
|
||||
- 主线程实施:
|
||||
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
|
||||
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests`、`TestArchitectureWithRegistry`、`TestResourceLoader`、`PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
|
||||
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op,定向回归测试通过
|
||||
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
|
||||
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
|
||||
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test`、`git diff --check`
|
||||
- 验证里程碑:
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 2026-04-28 — RP-091
|
||||
|
||||
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick,并升级 PR-review triage 规则
|
||||
|
||||
- 触发背景:
|
||||
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
|
||||
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md` 或 `$gframework-pr-review`
|
||||
- 主线程实施:
|
||||
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
|
||||
- 将 `TestArchitectureContext` 与 `TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
|
||||
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`,明确要求:latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 结果:成功;`67` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 活跃风险
|
||||
|
||||
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
|
||||
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
|
||||
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
|
||||
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
|
||||
|
||||
## 历史归档指针
|
||||
|
||||
- 最新 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- 历史 todo 归档:
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 早期归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
@ -0,0 +1,72 @@
|
||||
# Analyzer Warning Reduction 跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支,并保持 active recovery 文档只保留当前真值。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-096`
|
||||
- 当前阶段:`Completed`
|
||||
- 当前焦点:
|
||||
- `2026-04-29` 已完成 `PR #301` latest-head review threads 的最终本地复核,并修复仍然成立的空对象 `const` 比较键回归
|
||||
- 当前 topic 已达到归档条件:长期 warning-reduction 分支的实现、PR review follow-up 与最小验证均已完成
|
||||
- 当前目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,后续仅保留历史恢复价值
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前 `origin/main` 基线提交为 `0e32dab`(`2026-04-28T17:15:47+08:00`)。
|
||||
- 当前直接验证结果:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 最新结果:成功;`10` 通过、`0` 失败
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
|
||||
- 最新结果:成功;`25` 通过、`0` 失败
|
||||
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
|
||||
- 最新结果:成功;当前修复范围内无格式漂移
|
||||
- `git diff --check`
|
||||
- 最新结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前批次摘要:
|
||||
- 当前最终收尾切片直接修改 `3` 个已有文件,不再扩写 warning-batch 的多文件清理范围
|
||||
- 这次收尾把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 契约收窄为“允许空字符串,但拒绝非空纯空白”,恢复空对象 `const` / `enum` 的合法比较键语义
|
||||
- PR review triage 结论:
|
||||
- 接受并完成:并发共享状态、阻塞等待、无效约束状态、缺失 `<exception>` 文档、空对象比较键回归
|
||||
- 归档前剩余 open threads 只包含两类:尚未推送折叠的 stale 线程,以及已明确延后 / 驳回的建议(`DisplayPath` 与枚举特性泛化)
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 当前 GitHub PR 在本地提交并推送前仍可能显示旧的 open threads。
|
||||
- 缓解措施:以本文件中的本地验证结果为 archive 真值;若未来需要复查 PR 页面,应从 archive 恢复而不是重新激活 topic。
|
||||
- 本轮仅对 `GFramework.Game` 收尾回归做了受影响模块验证,没有重新建立新的仓库根 clean build 基线。
|
||||
- 缓解措施:后续若有新的 warning-reduction 任务,应创建新 topic,并重新执行仓库根 `dotnet clean` + `dotnet build` 采样。
|
||||
|
||||
## 活跃文档
|
||||
|
||||
- 当前轮次归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 历史跟踪归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- 历史 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 权威验证结果统一维护在“当前活跃事实”。
|
||||
- `GFramework.Game` 当前 Release 构建已清零,并通过空对象 `const` 回归与模型契约定向测试。
|
||||
- `GFramework.Cqrs.Tests` 当前 PR-review follow-up 定向测试通过,说明并发/缓存测试辅助实现的行为修正没有破坏现有集成断言。
|
||||
- `dotnet format --verify-no-changes` 已确认当前收尾改动未引入新的格式化偏差。
|
||||
- `git diff --check` 结果为空,说明本轮新增改动没有引入新的尾随空格或冲突标记。
|
||||
- 本 topic 已进入 archive;若未来重启 warning reduction,应以新 topic 和新的仓库级 clean build 基线继续。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
1. 保持当前 archive 状态,不要再把该 topic 作为默认 boot 入口。
|
||||
2. 若未来需要继续 warning reduction,创建新的 active topic,并重新建立仓库根 clean build 真值。
|
||||
@ -0,0 +1,209 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-29 — RP-096
|
||||
|
||||
### 阶段:完成 `PR #301` 最终收尾并归档长期 warning-reduction 主题
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先用 `$gframework-pr-review` 解决当前 PR review 的剩余问题,然后把整个长期分支主题归档
|
||||
- 本轮 triage 结论:
|
||||
- `MediatorArchitectureIntegrationTests` 并发更新、`YamlConfigConditionalSchemas` / `YamlConfigStringFormatConstraint` 的 `<exception>` 文档,以及两个枚举的 `[GenerateEnumExtensions]` 在当前工作树上均已存在,对应 open threads 判定为 stale
|
||||
- `YamlConfigReferenceUsage.DisplayPath` 删除建议继续判定为不成立,因为 loader 诊断、引用索引和测试断言仍把它作为稳定语义标签使用
|
||||
- `LoadAsync_Should_Accept_Empty_Object_Schema_Const` 失败仍然成立:上轮把 `YamlConfigAllowedValue` / `YamlConfigConstantValue` 的 `comparableValue` 收紧成 `ThrowIfNullOrWhiteSpace(...)` 后,误伤了空对象常量的合法空比较键
|
||||
- 主线程实施:
|
||||
- 将 `YamlConfigAllowedValue` 与 `YamlConfigConstantValue` 的比较键契约调整为:
|
||||
- 允许 `string.Empty`
|
||||
- 继续拒绝非空纯空白字符串
|
||||
- 保留 `displayValue` 的非空白要求
|
||||
- 扩充 `YamlConfigModelContractTests`,新增空比较键的正向覆盖,同时保留纯空白比较键的回归保护
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~LoadAsync_Should_Accept_Empty_Object_Schema_Const|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigAllowedValue.cs GFramework.Game/Config/YamlConfigConstantValue.cs GFramework.Game.Tests/Config/YamlConfigModelContractTests.cs`
|
||||
- 结果:成功
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 归档结论:
|
||||
- `analyzer-warning-reduction` 当前 topic 已满足归档条件:长期 warning-reduction 主线已收尾,PR #301 的本地 follow-up 闭环完成
|
||||
- 整个 topic 目录已迁入 `ai-plan/public/archive/analyzer-warning-reduction/`,不再作为 active 默认入口
|
||||
|
||||
## 2026-04-29 — RP-095
|
||||
|
||||
### 阶段:复核 `PR #301` latest-head review threads,并只修复当前工作树上仍然成立的问题
|
||||
|
||||
- 触发背景:
|
||||
- 用户显式要求执行 `$gframework-pr-review`,需要把 GitHub PR review 信号与本地代码现状重新核对,而不是沿用旧的 warning-batch 假设
|
||||
- 本轮 triage 结论:
|
||||
- 接受并修复:
|
||||
- `MediatorArchitectureIntegrationTests` 中 `Task.Delay().Wait()` 阻塞、静态 `Dictionary` 竞态、`SharedState.Counter +=` 非原子更新、以及 `TestNestedRequestHandler` 冗余分支
|
||||
- `GFramework.Game/Config` 中仍然成立的模型契约缺口:空白比较键、数组 / 对象边界非法状态、`Pattern` / `PatternRegex` 不一致、`ReferencedTableNames` 未做 defensive copy、以及缺失的 `<exception>` XML 文档
|
||||
- `MediatorAdvancedFeaturesTests` 中 `MA0048` 抑制缺少原因注释
|
||||
- `YamlConfigSchemaNode.NodeValidation.None` 未被引用,按 review 建议删除死代码
|
||||
- 明确不接受或延后:
|
||||
- `YamlConfigReferenceUsage.DisplayPath`:当前在 loader 诊断与测试断言中承担独立语义标签,不作为“纯冗余 alias”删除
|
||||
- `YamlConfigSchemaPropertyType` / `YamlConfigStringFormatKind` 补 `[GenerateEnumExtensions]`:仓库产品代码没有现成约定或使用面,判断为泛化误报
|
||||
- 主线程实施:
|
||||
- 将 CQRS 集成测试辅助处理器改为真正异步,并用 `ConcurrentDictionary` / `Interlocked` 收口并发共享状态
|
||||
- 为 `YamlConfigAllowedValue`、`YamlConfigConstantValue`、`YamlConfigArrayContainsConstraints`、`YamlConfigArrayConstraints`、`YamlConfigObjectConstraints`、`YamlConfigStringConstraints`、`YamlConfigSchema`、`YamlConfigConditionalSchemas`、`YamlConfigStringFormatConstraint` 补运行时契约或 `<exception>` 注释
|
||||
- 新增 `YamlConfigModelContractTests`,锁定上述模型拒绝无效状态的行为
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~YamlConfigModelContractTests"`
|
||||
- 第一次结果:成功;`10` 通过、`0` 失败,但新增测试触发 `MA0009`
|
||||
- 第二次结果:成功;`10` 通过、`0` 失败;为测试中的 `Regex` 补 timeout 后 warning 清零
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~MediatorArchitectureIntegrationTests|FullyQualifiedName~MediatorAdvancedFeaturesTests"`
|
||||
- 结果:成功;`25` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 下一步:
|
||||
- 提交当前 PR-review follow-up 与 `ai-plan` 同步
|
||||
- 推送后重新执行 `$gframework-pr-review`,确认 remaining open threads 是否已缩减到延后 / 误报项
|
||||
|
||||
## 2026-04-29 — RP-094
|
||||
|
||||
### 阶段:收尾 `YamlConfigSchemaValidator` 剩余 `MA0051` 并将仓库根 clean build 归零
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先拿构建 warning,再在 warning 很多时分批指派 subagent;本轮按 `$gframework-batch-boot 50` 继续执行
|
||||
- 基线与停机判断:
|
||||
- 当前 `origin/main` 仍为 `0e32dab`(`2026-04-28T17:15:47+08:00`)
|
||||
- 本轮标准仓库根 `dotnet clean` + `dotnet build` 直接成功;warning 总数为 `15`,但全部集中在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs`
|
||||
- 由于 `15` 条 warning 实际只对应同一文件内 `5` 个独立 `MA0051` 方法,不满足“warning 非常多且可安全分派多个独立写边界”的条件,因此不再新增 worker
|
||||
- 主线程实施:
|
||||
- 将 `ParseNode` 拆成 `ResolveNodeTypeName`、`ValidateObjectOnlyKeywords`、`CreateParsedNodeForType`
|
||||
- 将 `ValidateObjectNode` 拆成对象类型确认、属性遍历与 required 校验 helper
|
||||
- 将 `ValidateObjectConstraints` 拆成 property count、`dependentRequired`、`dependentSchemas`、`allOf`、条件分支五个 helper
|
||||
- 将 `ValidateScalarNode` 与 `ValidateNumericScalarConstraints` 分别拆成标量类型确认、引用回写、数值上下界和 `multipleOf` helper
|
||||
- 追加 `ValidateConditionalSchemaBranch` 收口 if/then/else 分支;随后修正该 helper 引入的 `MA0006`
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -clp:Summary`
|
||||
- 第一次结果:成功;`3` warnings、`0` errors(均为新 helper 中 `branchName == "then"` 引入的 `MA0006`)
|
||||
- 第二次结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||
- 结果:成功;`80` 通过、`0` 失败
|
||||
- `dotnet clean`
|
||||
- 结果:成功
|
||||
- `dotnet build`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前指标:
|
||||
- 仓库根 clean build warning:`15` -> `0`
|
||||
- 当前分支相对 `origin/main...HEAD` 仍为 `22` 个变更文件,低于 `$gframework-batch-boot 50` 的文件阈值
|
||||
- 当前停止原因:warning hotspot 已耗尽,不再有可重复切片
|
||||
- 下一步:
|
||||
- 提交 `YamlConfigSchemaValidator` 收尾重构与本轮 `ai-plan` 真值更新
|
||||
|
||||
## 2026-04-29 — RP-093
|
||||
|
||||
### 阶段:按 `$gframework-batch-boot 50` 从 clean build warning 基线分批清理
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求先拿构建 warning,再分批指派 subagent 加快处理;停止条件解析为分支相对 `origin/main` 接近 `50` 个变更文件
|
||||
- 基线与环境:
|
||||
- 当前 `origin/main` 为 `0e32dab`(`2026-04-28T17:15:47+08:00`)
|
||||
- 标准 `dotnet clean` 在当前 WSL 环境仍被 Windows NuGet fallback package folder 阻塞;按既有环境口径先执行 `dotnet restore GFramework.sln -p:RestoreFallbackFolders= --disable-parallel` 后,使用 `-p:RestoreFallbackFolders=` 完成 clean / build
|
||||
- clean 后 warning 基线:`236` warnings、`0` errors
|
||||
- 已接受的 worker 范围:
|
||||
- `ed269d4`:`GFramework.Cqrs.Tests/Mediator/MediatorArchitectureIntegrationTests.cs`,清理 `MA0048` / `MA0004` / `MA0016`
|
||||
- `121df44`:`GFramework.Cqrs.Tests/Mediator/MediatorAdvancedFeaturesTests.cs`,清理 `MA0048` / `MA0004` / `MA0015`
|
||||
- `9109eec`:`GFramework.Cqrs.Tests/Mediator/MediatorComprehensiveTests.cs`,清理 `MA0048` / `MA0004` / `MA0016` / `MA0002` / `MA0015`
|
||||
- 主线程实施:
|
||||
- 在 `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 为固定格式正则与 schema `pattern` 正则补充 timeout,避免运行时正则输入继续触发 `MA0009`
|
||||
- 将三处字符串等值比较改为 ordinal `string.Equals`,清理 `MA0006`
|
||||
- 接受 `1395b84` 的 `YamlConfigSchemaValidator.ObjectKeywords.cs` 方法拆分,清理该文件 `MA0051`
|
||||
- 收口被中止 worker 留下的 schema model 拆文件变更,将 `YamlConfigSchemaValidator.cs` 末尾类型移动到同名文件,清理 `MA0048`
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Mediator"`
|
||||
- 结果:成功;`45` 通过、`0` 失败
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false -clp:Summary`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~YamlConfigLoaderTests|FullyQualifiedName~YamlConfigSchemaValidatorTests"`
|
||||
- 结果:成功;`80` 通过、`0` 失败
|
||||
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
|
||||
- 结果:成功
|
||||
- `dotnet build -p:RestoreFallbackFolders= -clp:WarningsOnly -v:minimal -m:1 -nodeReuse:false`
|
||||
- 中间结果:成功;`75` warnings、`0` errors
|
||||
- `dotnet clean -p:RestoreFallbackFolders= -v:quiet`
|
||||
- 结果:成功
|
||||
- `dotnet build -p:RestoreFallbackFolders= -clp:Summary -v:minimal -m:1 -nodeReuse:false`
|
||||
- 结果:成功;`15 Warning(s)`、`0 Error(s)`
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
- 当前指标:
|
||||
- warning 总数:`236` -> `15`
|
||||
- 剩余 warning 分布:`GFramework.Game/Config/YamlConfigSchemaValidator.cs` 的 `MA0051` `15` 条(5 个方法跨 3 个 TFM)
|
||||
- 本轮提交后预计分支 diff:`22` 个文件,低于 `50` 个文件阈值
|
||||
- 下一步:
|
||||
- 按用户要求本轮到此结束;下一轮只处理 `YamlConfigSchemaValidator.cs` 剩余 `MA0051` 方法拆分
|
||||
|
||||
## 2026-04-28 — RP-092
|
||||
|
||||
### 阶段:复核 `PR #300` 的 open threads,并只修正当前分支仍然成立的 `ai-plan` 漂移
|
||||
|
||||
- 触发背景:
|
||||
- 用户要求恢复当前 `$gframework-pr-review` 任务,继续以 PR head 上的开放线程为准做 triage
|
||||
- 主线程实施:
|
||||
- 重新读取 `fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 的 latest head open threads
|
||||
- 逐条对照本地文件后确认:`TestArchitectureContextBehaviorTests`、`TestArchitectureWithRegistry`、`TestResourceLoader`、`PartialGeneratedNotificationHandlerRegistry` 相关 CodeRabbit 线程在当前工作树上都已匹配修复,仅线程状态尚未随新 head 折叠
|
||||
- 继续核对 `RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found`,确认当前实现 `RegistryInitializationHookBase.OnPhase` 已在缺少注册表时保持 no-op,定向回归测试通过
|
||||
- 修正 `analyzer-warning-reduction-tracking.md` 中仍然成立的两处漂移:
|
||||
- 将文件计数更新为相对 `6cc87a9...HEAD` 的实际规模:`18` 个已修改文件、`38` 个新增文件、合计 `56` 个变更文件
|
||||
- 将验证口径统一为 trace 已记录的 `dotnet build`、定向 `dotnet test`、`git diff --check`
|
||||
- 验证里程碑:
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~RegistryInitializationHookBaseTests.OnPhase_Should_Not_Throw_When_Registry_Not_Found|FullyQualifiedName~TestArchitectureContextBehaviorTests"`
|
||||
- 结果:成功;`10` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 2026-04-28 — RP-091
|
||||
|
||||
### 阶段:收口 `PR #300` 的共享测试基础设施 nitpick,并升级 PR-review triage 规则
|
||||
|
||||
- 触发背景:
|
||||
- 用户追问 `TestArchitectureContext` / `TestArchitectureContextV3` 的共享基础设施 nitpick 是否已经处理完成
|
||||
- 同时要求把“本地验证后仍然成立的 nitpick 不能默认降级为可选项”写入 `AGENTS.md` 或 `$gframework-pr-review`
|
||||
- 主线程实施:
|
||||
- 新增 `TestArchitectureContextBase`,把容器解析、共享 `EventBus` 行为,以及 legacy / CQRS 失败契约统一收敛到一处
|
||||
- 将 `TestArchitectureContext` 与 `TestArchitectureContextV3` 收窄为薄包装类型,只保留各自的命名入口与 `Id` 差异
|
||||
- 更新 `.agents/skills/gframework-pr-review/SKILL.md`,明确要求:latest-head `Nitpick comment` 一旦本地验证仍成立且指向真实漂移/回归风险,就必须作为 actionable review input 处理,而不是默认视作可选
|
||||
- 验证里程碑:
|
||||
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ArchitectureServicesTests|FullyQualifiedName~ContextAwareServiceExtensionsTests|FullyQualifiedName~TestArchitectureContextBehaviorTests|FullyQualifiedName~RegistryInitializationHookBaseTests|FullyQualifiedName~ArchitectureContextTests"`
|
||||
- 结果:成功;`67` 通过、`0` 失败
|
||||
- `git diff --check`
|
||||
- 结果:成功;无新增 whitespace / conflict-marker 问题
|
||||
|
||||
## 活跃风险
|
||||
|
||||
- GitHub PR 上的 open threads 在本地提交前仍可能显示为未关闭。
|
||||
- 缓解措施:以当前工作树和定向验证作为真值,推送后再让 PR 线程重新比对最新 head。
|
||||
- `GFramework.Core.Tests` 项目当前存在独立于本轮改动的 `dotnet format` 基线。
|
||||
- 缓解措施:保持为后续单独格式治理切片,不在当前 PR review follow-up 中扩写。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 提交本轮 `ai-plan` 同步修复,使 PR head 能重新折叠文档相关线程。
|
||||
2. 推送后重新执行 `$gframework-pr-review`,确认剩余 open threads 是否已经下降。
|
||||
|
||||
## 历史归档指针
|
||||
|
||||
- 最新 trace 归档:
|
||||
- [analyzer-warning-reduction-history-rp083-rp088.md](../archive/traces/analyzer-warning-reduction-history-rp083-rp088.md)
|
||||
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||
- 历史 todo 归档:
|
||||
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- 早期归档:
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||
Loading…
x
Reference in New Issue
Block a user