Files
MoShiro/backend/shared/MoShiro.Protocol/Catalog/ParamSpec.cs
T
justme 2e146d4da7
ci/woodpecker/push/main-amd64-build Pipeline was successful
refactor!: rename the project from Monitor to MoShiro [CI]
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
2026-10-07 02:03:25 +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);
}
}