using EllieBot.Common.Configs; using EllieBot.Common.Yml; using System.Linq.Expressions; using System.Reflection; namespace EllieBot.Services; /// <summary> /// Base service for all settings services /// </summary> /// <typeparam name="TSettings">Type of the settings</typeparam> public abstract class ConfigServiceBase<TSettings> : IConfigService where TSettings : ICloneable<TSettings>, new() { // FUTURE config arrays are not copied - they're not protected from mutations public TSettings Data => data.Clone(); public abstract string Name { get; } protected readonly string _filePath; protected readonly IConfigSeria _serializer; protected readonly IPubSub _pubSub; private readonly TypedKey<TSettings> _changeKey; protected TSettings data; private readonly Dictionary<string, Func<TSettings, string, bool>> _propSetters = new(); private readonly Dictionary<string, Func<object>> _propSelectors = new(); private readonly Dictionary<string, Func<object, string>> _propPrinters = new(); private readonly Dictionary<string, string?> _propComments = new(); /// <summary> /// Initialized an instance of <see cref="ConfigServiceBase{TSettings}" /> /// </summary> /// <param name="filePath">Path to the file where the settings are serialized/deserialized to and from</param> /// <param name="serializer">Serializer which will be used</param> /// <param name="pubSub">Pubsub implementation for signaling when settings are updated</param> /// <param name="changeKey">Key used to signal changed event</param> protected ConfigServiceBase( string filePath, IConfigSeria serializer, IPubSub pubSub, TypedKey<TSettings> changeKey) { _filePath = filePath; _serializer = serializer; _pubSub = pubSub; _changeKey = changeKey; data = new(); Load(); _pubSub.Sub(_changeKey, OnChangePublished); } private void PublishChange() => _pubSub.Pub(_changeKey, data); private ValueTask OnChangePublished(TSettings newData) { data = newData; OnStateUpdate(); return default; } /// <summary> /// Loads data from disk. If file doesn't exist, it will be created with default values /// </summary> protected void Load() { // if file is deleted, regenerate it with default values if (!File.Exists(_filePath)) { data = new(); Save(); } try { data = _serializer.Deserialize<TSettings>(File.ReadAllText(_filePath)); } catch (Exception ex) { Log.Error(ex, "Error while loading {ConfigFilePath}", _filePath); throw; } } /// <summary> /// Loads new data and publishes the new state /// </summary> public void Reload() { Load(); _pubSub.Pub(_changeKey, data); } /// <summary> /// Doesn't do anything by default. This method will be executed after /// <see cref="data" /> is reloaded from <see cref="_filePath" /> or new data is recieved /// from the publish event /// </summary> protected virtual void OnStateUpdate() { } private void Save() { var strData = _serializer.Serialize(data); File.WriteAllText(_filePath, strData); } protected void AddParsedProp<TProp>( string key, Expression<Func<TSettings, TProp>> selector, SettingParser<TProp> parser, Func<TProp, string> printer, Func<TProp, bool>? checker = null) { checker ??= _ => true; key = key.ToLowerInvariant(); _propPrinters[key] = obj => printer((TProp)obj); _propSelectors[key] = () => selector.Compile()(data)!; _propSetters[key] = Magic(selector, parser, checker); _propComments[key] = ((MemberExpression)selector.Body).Member.GetCustomAttribute<CommentAttribute>()?.Comment; } private Func<TSettings, string, bool> Magic<TProp>( Expression<Func<TSettings, TProp>> selector, SettingParser<TProp> parser, Func<TProp, bool> checker) => (target, input) => { if (!parser(input, out var value)) return false; if (!checker(value)) return false; object targetObject = target; var expr = (MemberExpression)selector.Body; var prop = (PropertyInfo)expr.Member; var expressions = new List<MemberExpression>(); while (true) { expr = expr.Expression as MemberExpression; if (expr is null) break; expressions.Add(expr); } foreach (var memberExpression in expressions.AsEnumerable().Reverse()) { var localProp = (PropertyInfo)memberExpression.Member; targetObject = localProp.GetValue(targetObject)!; } prop.SetValue(targetObject, value, null); return true; }; public IReadOnlyList<string> GetSettableProps() => _propSetters.Keys.ToList(); public string? GetSetting(string prop) { prop = prop.ToLowerInvariant(); if (!_propSelectors.TryGetValue(prop, out var selector) || !_propPrinters.TryGetValue(prop, out var printer)) return null; return printer(selector()); } public string? GetComment(string prop) { if (_propComments.TryGetValue(prop, out var comment)) return comment; return null; } private bool SetProperty(TSettings target, string key, string value) => _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic) && magic(target, value); public bool SetSetting(string prop, string newValue) { var success = true; ModifyConfig(bs => { success = SetProperty(bs, prop, newValue); }); if (success) PublishChange(); return success; } public void ModifyConfig(Action<TSettings> action) { var copy = Data; action(copy); data = copy; Save(); PublishChange(); } }