Files
justme 83e5120377 refactor!: rename the project from Monitor to MoShiro
Namespaces, assemblies, projects and directories, the solution, binaries,
images, systemd units, config paths, environment variables, protocol method
names, the media type and the raw chunk magic all move to MoShiro/moshiro.
Existing data and deployments are not compatible and must be recreated.
Also fix the architecture tests after the solution layout change and make
the update tests independent of their order.
2026-10-07 01:49:04 +08:00

224 lines
7.6 KiB
C#

using System.Globalization;
using System.Text.Json;
namespace MoShiro.Protocol;
public enum ParamType
{
Int,
Number,
Bool,
Enum,
}
/// <summary>
/// 参数定义:类型、默认值、上下限。节点与 Backend 使用同一套校验,禁止写没有上下限的参数。
/// </summary>
public abstract record ParamSpec(string Name, string Description, string? Unit)
{
public abstract ParamType Type { get; }
/// <summary>用于目录展示的默认值。</summary>
public abstract JsonElement DefaultValue { get; }
/// <summary>校验并规范化一个参数值;失败时返回错误说明。</summary>
public abstract bool TryNormalize(JsonElement input, out JsonElement normalized, out string error);
protected static JsonElement Of(long value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Int64);
protected static JsonElement Of(double value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Double);
protected static JsonElement Of(bool value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.Boolean);
protected static JsonElement Of(string value) => JsonSerializer.SerializeToElement(value, ParamJson.Default.String);
}
public sealed record IntParam(string Name, long Default, long Min, long Max, string? Unit = null, string Description = "")
: ParamSpec(Name, Description, Unit)
{
public override ParamType Type => ParamType.Int;
public override JsonElement DefaultValue => Of(Default);
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
{
normalized = default;
error = "";
if (input.ValueKind != JsonValueKind.Number || !input.TryGetInt64(out long value))
{
error = "must be an integer";
return false;
}
if (value < Min || value > Max)
{
error = $"must be between {Min.ToString(CultureInfo.InvariantCulture)} and {Max.ToString(CultureInfo.InvariantCulture)}";
return false;
}
normalized = Of(value);
return true;
}
}
public sealed record NumberParam(string Name, double Default, double Min, double Max, string? Unit = null, string Description = "")
: ParamSpec(Name, Description, Unit)
{
public override ParamType Type => ParamType.Number;
public override JsonElement DefaultValue => Of(Default);
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
{
normalized = default;
error = "";
if (input.ValueKind != JsonValueKind.Number || !input.TryGetDouble(out double value) || !double.IsFinite(value))
{
error = "must be a number";
return false;
}
if (value < Min || value > Max)
{
error = $"must be between {Min.ToString(CultureInfo.InvariantCulture)} and {Max.ToString(CultureInfo.InvariantCulture)}";
return false;
}
normalized = Of(value);
return true;
}
}
public sealed record BoolParam(string Name, bool Default, string Description = "")
: ParamSpec(Name, Description, null)
{
public override ParamType Type => ParamType.Bool;
public override JsonElement DefaultValue => Of(Default);
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
{
normalized = default;
error = "";
if (input.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
{
error = "must be a boolean";
return false;
}
normalized = Of(input.GetBoolean());
return true;
}
}
public sealed record EnumParam(string Name, string Default, IReadOnlyList<string> Values, string Description = "")
: ParamSpec(Name, Description, null)
{
public override ParamType Type => ParamType.Enum;
public override JsonElement DefaultValue => Of(Default);
public override bool TryNormalize(JsonElement input, out JsonElement normalized, out string error)
{
normalized = default;
error = "";
string? value = input.ValueKind == JsonValueKind.String ? input.GetString() : null;
if (value is null || !Values.Contains(value))
{
error = "must be one of: " + string.Join(", ", Values);
return false;
}
normalized = Of(value);
return true;
}
}
/// <summary>参数声明的简写。</summary>
public static class P
{
public static ParamSpec Int(string name, long @default, long min, long max, string? unit = null, string description = "") =>
new IntParam(name, @default, min, max, unit, description);
public static ParamSpec Number(string name, double @default, double min, double max, string? unit = null, string description = "") =>
new NumberParam(name, @default, min, max, unit, description);
public static ParamSpec Bool(string name, bool @default, string description = "") => new BoolParam(name, @default, description);
public static ParamSpec Enum(string name, string @default, string[] values, string description = "") =>
new EnumParam(name, @default, values, description);
}
[System.Text.Json.Serialization.JsonSerializable(typeof(long))]
[System.Text.Json.Serialization.JsonSerializable(typeof(double))]
[System.Text.Json.Serialization.JsonSerializable(typeof(bool))]
[System.Text.Json.Serialization.JsonSerializable(typeof(string))]
internal sealed partial class ParamJson : System.Text.Json.Serialization.JsonSerializerContext;
/// <summary>已经校验并补全了默认值的参数。</summary>
public sealed class ParamValues
{
private readonly Dictionary<string, JsonElement> _values;
public ParamValues(Dictionary<string, JsonElement> values)
{
_values = values;
}
public IReadOnlyDictionary<string, JsonElement> Values => _values;
public long GetInt(string name) => _values[name].GetInt64();
public double GetNumber(string name) => _values[name].GetDouble();
public bool GetBool(string name) => _values[name].GetBoolean();
public string GetString(string name) => _values[name].GetString()!;
}
public sealed record ParamResolution(ParamValues? Values, IReadOnlyDictionary<string, string> Errors)
{
public bool IsValid => Errors.Count == 0;
}
public static class ParamResolver
{
/// <summary>未知参数视为错误;缺失的参数取默认值。</summary>
public static ParamResolution Resolve(IReadOnlyList<ParamSpec> specs, IReadOnlyDictionary<string, JsonElement>? input)
{
var errors = new Dictionary<string, string>();
var values = new Dictionary<string, JsonElement>(specs.Count);
foreach (ParamSpec spec in specs)
{
if (input is not null && input.TryGetValue(spec.Name, out JsonElement raw))
{
if (spec.TryNormalize(raw, out JsonElement normalized, out string error))
{
values[spec.Name] = normalized;
}
else
{
errors[spec.Name] = error;
}
}
else
{
values[spec.Name] = spec.DefaultValue;
}
}
if (input is not null)
{
foreach (string key in input.Keys)
{
if (!values.ContainsKey(key) && !errors.ContainsKey(key))
{
errors[key] = "is not a parameter of this kind";
}
}
}
return new ParamResolution(errors.Count == 0 ? new ParamValues(values) : null, errors);
}
}