ci/woodpecker/push/main-amd64-build Pipeline was successful
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.
224 lines
7.6 KiB
C#
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);
|
|
}
|
|
}
|