feat: ConfigPreflight raw-config aggregator
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent aggregator for validating raw <see cref="IConfiguration"/> BEFORE the host/DI container
|
||||
/// exists (e.g. pre-Akka startup). Collects all failures and surfaces them together via
|
||||
/// <see cref="ThrowIfInvalid"/>. For options that flow through DI, prefer
|
||||
/// <see cref="ServiceCollectionExtensions.AddValidatedOptions{TOptions, TValidator}"/>.
|
||||
/// </summary>
|
||||
public sealed class ConfigPreflight
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly List<string> _failures = [];
|
||||
|
||||
private ConfigPreflight(IConfiguration configuration) => _configuration = configuration;
|
||||
|
||||
/// <summary>Starts a preflight over <paramref name="configuration"/>.</summary>
|
||||
public static ConfigPreflight For(IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
return new ConfigPreflight(configuration);
|
||||
}
|
||||
|
||||
/// <summary>The accumulated failure messages (empty when valid).</summary>
|
||||
public IReadOnlyList<string> Failures => _failures;
|
||||
|
||||
/// <summary>True when no failures have been accumulated.</summary>
|
||||
public bool IsValid => _failures.Count == 0;
|
||||
|
||||
/// <summary>Requires the value at <paramref name="key"/> to satisfy <paramref name="predicate"/>.</summary>
|
||||
public ConfigPreflight Require(string key, Func<string?, bool> predicate, string reason)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
if (!predicate(_configuration[key])) _failures.Add($"{key} {reason}");
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Requires a non-empty value at <paramref name="key"/>.</summary>
|
||||
public ConfigPreflight RequireValue(string key) => AddIf(Checks.Required(_configuration[key], key));
|
||||
|
||||
/// <summary>Requires a valid integer TCP port (1-65535) at <paramref name="key"/>.</summary>
|
||||
public ConfigPreflight RequirePort(string key)
|
||||
{
|
||||
var raw = _configuration[key];
|
||||
if (!int.TryParse(raw, out var port))
|
||||
{
|
||||
_failures.Add($"{key} must be an integer port 1-65535 (was '{raw ?? "null"}')");
|
||||
return this;
|
||||
}
|
||||
return AddIf(Checks.Port(port, key));
|
||||
}
|
||||
|
||||
/// <summary>Runs <paramref name="block"/> only when <paramref name="condition"/> holds (role-conditional rules).</summary>
|
||||
public ConfigPreflight When(bool condition, Action<ConfigPreflight> block)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(block);
|
||||
if (condition) block(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Throws <see cref="InvalidOperationException"/> listing all failures when invalid; otherwise returns.</summary>
|
||||
public void ThrowIfInvalid()
|
||||
{
|
||||
if (_failures.Count > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Configuration validation failed:\n{string.Join("\n", _failures.Select(e => $" - {e}"))}");
|
||||
}
|
||||
|
||||
private ConfigPreflight AddIf(string? message)
|
||||
{
|
||||
if (message is not null) _failures.Add(message);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user