using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using JetBrains.Annotations; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; namespace Kyoo { /// /// A class containing helper methods to merge objects. /// public static class Merger { /// /// Merge two lists, can keep duplicates or remove them. /// /// The first enumerable to merge /// The second enumerable to merge, if items from this list are equals to one from the first, they are not kept /// Equality function to compare items. If this is null, duplicated elements are kept /// The two list merged as an array [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] public static T[] MergeLists([CanBeNull] IEnumerable first, [CanBeNull] IEnumerable second, [CanBeNull] Func isEqual = null) { if (first == null) return second?.ToArray(); if (second == null) return first.ToArray(); if (isEqual == null) return first.Concat(second).ToArray(); List list = first.ToList(); return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray(); } /// /// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept. /// /// The first dictionary to merge /// The second dictionary to merge /// The type of the keys in dictionaries /// The type of values in the dictionaries /// The first dictionary with the missing elements of . /// [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] public static IDictionary MergeDictionaries([CanBeNull] IDictionary first, [CanBeNull] IDictionary second) { return MergeDictionaries(first, second, out bool _); } /// /// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept. /// /// The first dictionary to merge /// The second dictionary to merge /// /// true if a new items has been added to the dictionary, false otherwise. /// /// The type of the keys in dictionaries /// The type of values in the dictionaries /// The first dictionary with the missing elements of . [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] public static IDictionary MergeDictionaries([CanBeNull] IDictionary first, [CanBeNull] IDictionary second, out bool hasChanged) { if (first == null) { hasChanged = true; return second; } hasChanged = false; if (second == null) return first; foreach ((T key, T2 value) in second) { bool success = first.TryAdd(key, value); hasChanged |= success; if (success || first[key]?.Equals(default) == false || value?.Equals(default) != false) continue; first[key] = value; hasChanged = true; } return first; } /// /// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept. /// /// /// The only difference in this function compared to /// /// is the way is calculated and the order of the arguments. /// /// MergeDictionaries(first, second); /// /// will do the same thing as /// /// CompleteDictionaries(second, first, out bool _); /// /// /// The first dictionary to merge /// The second dictionary to merge /// /// true if a new items has been added to the dictionary, false otherwise. /// /// The type of the keys in dictionaries /// The type of values in the dictionaries /// /// A dictionary with the missing elements of /// set to those of . /// [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] public static IDictionary CompleteDictionaries([CanBeNull] IDictionary first, [CanBeNull] IDictionary second, out bool hasChanged) { if (first == null) { hasChanged = true; return second; } hasChanged = false; if (second == null) return first; hasChanged = second.Any(x => x.Value?.Equals(first[x.Key]) == false); foreach ((T key, T2 value) in first) second.TryAdd(key, value); return second; } /// /// Set every fields of first to those of second. Ignore fields marked with the attribute /// At the end, the OnMerge method of first will be called if first is a /// /// The object to assign /// The object containing new values /// Fields of T will be used /// public static T Assign(T first, T second) { Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); foreach (PropertyInfo property in properties) { object value = property.GetValue(second); property.SetValue(first, value); } if (first is IOnMerge merge) merge.OnMerge(second); return first; } /// /// Set every non-default values of seconds to the corresponding property of second. /// Dictionaries are handled like anonymous objects with a property per key/pair value /// (see /// /// for more details). /// At the end, the OnMerge method of first will be called if first is a /// /// /// This does the opposite of . /// /// /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"} /// /// /// The object to complete /// /// /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. /// /// /// Filter fields that will be merged /// /// Fields of T will be completed /// /// If first is null public static T Complete([NotNull] T first, [CanBeNull] T second, [InstantHandle] Func where = null) { if (first == null) throw new ArgumentNullException(nameof(first)); if (second == null) return first; Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) properties = properties.Where(where); foreach (PropertyInfo property in properties) { object value = property.GetValue(second); object defaultValue = property.GetCustomAttribute()?.Value ?? property.PropertyType.GetClrDefault(); if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first))) continue; if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) { Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) .GenericTypeArguments; object[] parameters = { property.GetValue(first), value, false }; object newDictionary = Utility.RunGenericMethod( typeof(Merger), nameof(CompleteDictionaries), dictionaryTypes, parameters); if ((bool)parameters[2]) property.SetValue(first, newDictionary); } else property.SetValue(first, value); } if (first is IOnMerge merge) merge.OnMerge(second); return first; } /// /// This will set missing values of to the corresponding values of . /// Enumerable will be merged (concatenated) and Dictionaries too. /// At the end, the OnMerge method of first will be called if first is a . /// /// /// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"} /// /// /// The object to complete /// /// /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. /// /// /// Filter fields that will be merged /// /// Fields of T will be merged /// [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] public static T Merge([CanBeNull] T first, [CanBeNull] T second, [InstantHandle] Func where = null) { if (first == null) return second; if (second == null) return first; Type type = typeof(T); IEnumerable properties = type.GetProperties() .Where(x => x.CanRead && x.CanWrite && Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null); if (where != null) properties = properties.Where(where); foreach (PropertyInfo property in properties) { object oldValue = property.GetValue(first); object newValue = property.GetValue(second); object defaultValue = property.PropertyType.GetClrDefault(); if (oldValue?.Equals(defaultValue) != false) property.SetValue(first, newValue); else if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) { Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) .GenericTypeArguments; object[] parameters = { oldValue, newValue, false }; object newDictionary = Utility.RunGenericMethod( typeof(Merger), nameof(MergeDictionaries), dictionaryTypes, parameters); if ((bool)parameters[2]) property.SetValue(first, newDictionary); } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string)) { Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>)) .GenericTypeArguments .First(); Func equalityComparer = enumerableType.IsAssignableTo(typeof(IResource)) ? (x, y) => x.Slug == y.Slug : null; property.SetValue(first, Utility.RunGenericMethod( typeof(Merger), nameof(MergeLists), enumerableType, oldValue, newValue, equalityComparer)); } } if (first is IOnMerge merge) merge.OnMerge(second); return first; } /// /// Set every fields of to the default value. /// /// The object to nullify /// Fields of T will be nullified /// public static T Nullify(T obj) { Type type = typeof(T); foreach (PropertyInfo property in type.GetProperties()) { if (!property.CanWrite || property.GetCustomAttribute() != null) continue; property.SetValue(obj, property.PropertyType.GetClrDefault()); } return obj; } } }