using System.Globalization; using System.Text.Json; namespace MoShiro.Protocol; public enum ParamType { Int, Number, Bool, Enum, } /// /// 参数定义:类型、默认值、上下限。节点与 Backend 使用同一套校验,禁止写没有上下限的参数。 /// public abstract record ParamSpec(string Name, string Description, string? Unit) { public abstract ParamType Type { get; } /// 用于目录展示的默认值。 public abstract JsonElement DefaultValue { get; } /// 校验并规范化一个参数值;失败时返回错误说明。 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 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; } } /// 参数声明的简写。 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; /// 已经校验并补全了默认值的参数。 public sealed class ParamValues { private readonly Dictionary _values; public ParamValues(Dictionary values) { _values = values; } public IReadOnlyDictionary 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 Errors) { public bool IsValid => Errors.Count == 0; } public static class ParamResolver { /// 未知参数视为错误;缺失的参数取默认值。 public static ParamResolution Resolve(IReadOnlyList specs, IReadOnlyDictionary? input) { var errors = new Dictionary(); var values = new Dictionary(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); } }