// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections; using System.Reflection; using EpicGames.Core; namespace HordeServer.Configuration { /// /// Possible methods for merging config values /// public enum ConfigMergeStrategy { /// /// Default strategy; replace with the base value if the current value is null /// Default, /// /// Append the contents of this list to the base list /// Append, /// /// Recursively merge object properties /// Recursive, } /// /// Attribute used to mark object properties whose child properties should be merged individually /// [AttributeUsage(AttributeTargets.Property)] public sealed class ConfigMergeStrategyAttribute : Attribute { /// /// Strategy for merging this property /// public ConfigMergeStrategy Strategy { get; } /// /// Constructor /// public ConfigMergeStrategyAttribute(ConfigMergeStrategy strategy) => Strategy = strategy; } /// /// Helper methods for merging default values /// public static class ConfigObject { /// /// Merges any unassigned properties from the source object to the target /// /// Set of objects to use for merging public static void MergeDefaults(IEnumerable<(TKey Key, TKey? BaseKey, TValue Value)> objects) where TKey : notnull where TValue : class { // Convert the objects to a dictionary List<(TKey Key, TKey BaseKey, TValue Value)> remainingObjects = new List<(TKey Key, TKey BaseKey, TValue Value)>(); // Find all the objects with no base Dictionary handledValues = new Dictionary(); foreach ((TKey key, TKey? baseKey, TValue value) in objects) { #pragma warning disable CA1508 // Avoid dead conditional code (false positive due to generics) if (baseKey == null || Equals(baseKey, default(TKey))) { handledValues.Add(key, value); } else { remainingObjects.Add((key, baseKey, value)); } #pragma warning restore CA1508 // Avoid dead conditional code } // Iteratively merge objects with their base for (int lastRemainingObjectCount = 0; remainingObjects.Count != lastRemainingObjectCount;) { lastRemainingObjectCount = remainingObjects.Count; for (int idx = remainingObjects.Count - 1; idx >= 0; idx--) { (TKey key, TKey baseKey, TValue value) = remainingObjects[idx]; if (handledValues.TryGetValue(baseKey, out TValue? baseValue)) { MergeDefaults(value, baseValue); handledValues.Add(key, value); remainingObjects.RemoveAt(idx); } } } // Check we were able to merge everything if (remainingObjects.Count > 0) { HashSet validKeys = new HashSet(objects.Select(x => x.Key)); foreach ((TKey key, TKey baseKey, TValue value) in remainingObjects) { if (!validKeys.Contains(baseKey!)) { throw new Exception($"{key} has invalid/missing base {baseKey}"); } } List circularKeys = objects.Where(x => !handledValues.ContainsKey(x.Key)).Select(x => x.Key).ToList(); if (circularKeys.Count == 1) { throw new Exception($"{circularKeys[0]} has a circular dependency on itself"); } else { throw new Exception($"{StringUtils.FormatList(circularKeys.Select(x => x.ToString() ?? "(unknown)"))} have circular dependencies."); } } } /// /// Merges any unassigned properties from the source object to the target /// /// Target object to merge into /// Source object to merge from public static void MergeDefaults(T target, T source) where T : class { MergeDefaults(typeof(T), target, source); } /// /// Merges any unassigned properties from the source object to the target /// /// Type of the referenced object /// Target object to merge into /// Source object to merge from public static void MergeDefaults(Type type, object target, object source) { foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { object? targetValue = propertyInfo.GetValue(target); object? sourceValue = propertyInfo.GetValue(source); ConfigMergeStrategy strategy = propertyInfo.GetCustomAttribute()?.Strategy ?? ConfigMergeStrategy.Default; switch (strategy) { case ConfigMergeStrategy.Default: if (targetValue == null) { propertyInfo.SetValue(target, sourceValue); } break; case ConfigMergeStrategy.Append: if (targetValue == null) { propertyInfo.SetValue(target, sourceValue); } else if (sourceValue != null) { IList sourceList = (IList)sourceValue; IList targetList = (IList)targetValue; for (int idx = 0; idx < sourceList.Count; idx++) { targetList.Insert(idx, sourceList[idx]); } } break; case ConfigMergeStrategy.Recursive: if (targetValue == null) { propertyInfo.SetValue(target, sourceValue); } else if (sourceValue != null) { MergeDefaults(propertyInfo.PropertyType, targetValue, sourceValue); } break; } } } } }