Files
UnrealEngine/Engine/Source/Programs/Horde/HordeServer.Shared/Configuration/ConfigObject.cs
2025-05-18 13:04:45 +08:00

181 lines
5.5 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System.Collections;
using System.Reflection;
using EpicGames.Core;
namespace HordeServer.Configuration
{
/// <summary>
/// Possible methods for merging config values
/// </summary>
public enum ConfigMergeStrategy
{
/// <summary>
/// Default strategy; replace with the base value if the current value is null
/// </summary>
Default,
/// <summary>
/// Append the contents of this list to the base list
/// </summary>
Append,
/// <summary>
/// Recursively merge object properties
/// </summary>
Recursive,
}
/// <summary>
/// Attribute used to mark object properties whose child properties should be merged individually
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ConfigMergeStrategyAttribute : Attribute
{
/// <summary>
/// Strategy for merging this property
/// </summary>
public ConfigMergeStrategy Strategy { get; }
/// <summary>
/// Constructor
/// </summary>
public ConfigMergeStrategyAttribute(ConfigMergeStrategy strategy) => Strategy = strategy;
}
/// <summary>
/// Helper methods for merging default values
/// </summary>
public static class ConfigObject
{
/// <summary>
/// Merges any unassigned properties from the source object to the target
/// </summary>
/// <param name="objects">Set of objects to use for merging</param>
public static void MergeDefaults<TKey, TValue>(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<TKey, TValue> handledValues = new Dictionary<TKey, TValue>();
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<TKey> validKeys = new HashSet<TKey>(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<TKey> 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.");
}
}
}
/// <summary>
/// Merges any unassigned properties from the source object to the target
/// </summary>
/// <param name="target">Target object to merge into</param>
/// <param name="source">Source object to merge from</param>
public static void MergeDefaults<T>(T target, T source) where T : class
{
MergeDefaults(typeof(T), target, source);
}
/// <summary>
/// Merges any unassigned properties from the source object to the target
/// </summary>
/// <param name="type">Type of the referenced object</param>
/// <param name="target">Target object to merge into</param>
/// <param name="source">Source object to merge from</param>
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<ConfigMergeStrategyAttribute>()?.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;
}
}
}
}
}