Merger: Fixing Dictionary complete and merge

This commit is contained in:
Zoe Roux 2021-07-31 22:50:45 +02:00
parent 0fa73b1d6a
commit 181eb5ba2e
5 changed files with 191 additions and 18 deletions

View File

@ -44,22 +44,89 @@ namespace Kyoo
/// <param name="second">The second dictionary to merge</param> /// <param name="second">The second dictionary to merge</param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam> /// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam> /// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>A dictionary containing the result of the merge.</returns> /// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
/// <seealso cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2},out bool)"/>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)] [ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first, public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second) [CanBeNull] IDictionary<T, T2> second)
{
return MergeDictionaries(first, second, out bool _);
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> second,
out bool hasChanged)
{ {
if (first == null) if (first == null)
{
hasChanged = true;
return second; return second;
}
hasChanged = false;
if (second == null) if (second == null)
return first; return first;
Dictionary<T, T2> merged = new();
merged.EnsureCapacity(first.Count + second.Count);
foreach ((T key, T2 value) in first)
merged.Add(key, value);
foreach ((T key, T2 value) in second) foreach ((T key, T2 value) in second)
merged.TryAdd(key, value); hasChanged |= first.TryAdd(key, value);
return merged; return first;
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
/// </summary>
/// <remarks>
/// The only difference in this function compared to
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2}, out bool)"/>
/// is the way <paramref name="hasChanged"/> is calculated and the order of the arguments.
/// <code>
/// MergeDictionaries(first, second);
/// </code>
/// will do the same thing as
/// <code>
/// CompleteDictionaries(second, first, out bool _);
/// </code>
/// </remarks>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>
/// A dictionary with the missing elements of <paramref name="second"/>
/// set to those of <paramref name="first"/>.
/// </returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> CompleteDictionaries<T, T2>([CanBeNull] IDictionary<T, T2> first,
[CanBeNull] IDictionary<T, T2> 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]));
foreach ((T key, T2 value) in first)
second.TryAdd(key, value);
return second;
} }
/// <summary> /// <summary>
@ -91,7 +158,9 @@ namespace Kyoo
/// <summary> /// <summary>
/// Set every non-default values of seconds to the corresponding property of second. /// 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 /// Dictionaries are handled like anonymous objects with a property per key/pair value
/// (see <see cref="MergeDictionaries{T,T2}"/> for more details). /// (see
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2})"/>
/// for more details).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/> /// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary> /// </summary>
/// <remarks> /// <remarks>
@ -135,17 +204,24 @@ namespace Kyoo
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? property.PropertyType.GetClrDefault(); ?? property.PropertyType.GetClrDefault();
if (value?.Equals(defaultValue) != false || value == property.GetValue(first)) if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first)))
continue; continue;
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>))) if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{ {
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments; .GenericTypeArguments;
property.SetValue(first, Utility.RunGenericMethod<object>( object[] parameters = {
property.GetValue(first),
value,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger), typeof(Merger),
nameof(MergeDictionaries), nameof(CompleteDictionaries),
dictionaryTypes, dictionaryTypes,
value, property.GetValue(first))); parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
} }
else else
property.SetValue(first, value); property.SetValue(first, value);
@ -205,11 +281,18 @@ namespace Kyoo
{ {
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>)) Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments; .GenericTypeArguments;
property.SetValue(first, Utility.RunGenericMethod<object>( object[] parameters = {
oldValue,
newValue,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger), typeof(Merger),
nameof(MergeDictionaries), nameof(MergeDictionaries),
dictionaryTypes, dictionaryTypes,
oldValue, newValue)); parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
} }
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string)) && property.PropertyType != typeof(string))

View File

@ -325,7 +325,7 @@ namespace Kyoo
if (types.Length < 1) if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed."); throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args); MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args.ToArray()); return (T)method.MakeGenericMethod(types).Invoke(null, args);
} }
/// <summary> /// <summary>

View File

@ -99,7 +99,7 @@ namespace Kyoo.Tests.Database
value.Name = "New Title"; value.Name = "New Title";
value.Images = new Dictionary<int, string> value.Images = new Dictionary<int, string>
{ {
[Images.Poster] = "poster" [Images.Poster] = "new-poster"
}; };
await _repository.Edit(value, false); await _repository.Edit(value, false);

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Attributes; using Kyoo.Models.Attributes;
using Xunit; using Xunit;
@ -365,5 +366,94 @@ namespace Kyoo.Tests.Utility
Assert.Equal("thumbnails", ret.Images[Images.Thumbnail]); Assert.Equal("thumbnails", ret.Images[Images.Thumbnail]);
Assert.Equal("logo", ret.Images[Images.Logo]); Assert.Equal("logo", ret.Images[Images.Logo]);
} }
[Fact]
public void CompleteDictionaryOutParam()
{
Dictionary<int, string> first = new()
{
[Images.Logo] = "logo",
[Images.Poster] = "poster"
};
Dictionary<int, string> second = new()
{
[Images.Poster] = "new-poster",
[Images.Thumbnail] = "thumbnails"
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(3, ret.Count);
Assert.Equal("new-poster", ret[Images.Poster]);
Assert.Equal("thumbnails", ret[Images.Thumbnail]);
Assert.Equal("logo", ret[Images.Logo]);
}
[Fact]
public void CompleteDictionaryEqualTest()
{
Dictionary<int, string> first = new()
{
[Images.Poster] = "poster"
};
Dictionary<int, string> second = new()
{
[Images.Poster] = "new-poster",
};
IDictionary<int, string> ret = Merger.CompleteDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(1, ret.Count);
Assert.Equal("new-poster", ret[Images.Poster]);
}
private class TestMergeSetter
{
public Dictionary<int, int> Backing;
[UsedImplicitly] public Dictionary<int, int> Dictionary
{
get => Backing;
set
{
Backing = value;
KAssert.Fail();
}
}
}
[Fact]
public void CompleteDictionaryNoChangeNoSetTest()
{
TestMergeSetter first = new()
{
Backing = new Dictionary<int, int>
{
[2] = 3
}
};
TestMergeSetter second = new()
{
Backing = new Dictionary<int, int>()
};
Merger.Complete(first, second);
// This should no call the setter of first so the test should pass.
}
[Fact]
public void MergeDictionaryNoChangeNoSetTest()
{
TestMergeSetter first = new()
{
Backing = new Dictionary<int, int>
{
[2] = 3
}
};
TestMergeSetter second = new()
{
Backing = new Dictionary<int, int>()
};
Merger.Merge(first, second);
// This should no call the setter of first so the test should pass.
}
} }
} }

View File

@ -73,7 +73,7 @@ namespace Kyoo.Controllers
{ {
id.Provider = await _providers.CreateIfNotExists(id.Provider); id.Provider = await _providers.CreateIfNotExists(id.Provider);
id.ProviderID = id.Provider.ID; id.ProviderID = id.Provider.ID;
_database.Entry(id.Provider).State = EntityState.Detached; _database.Entry(id.Provider).State = EntityState.Unchanged;
} }
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs); _database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
} }
@ -82,7 +82,7 @@ namespace Kyoo.Controllers
/// <inheritdoc /> /// <inheritdoc />
protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld) protected override async Task EditRelations(Collection resource, Collection changed, bool resetOld)
{ {
await Validate(resource); await Validate(changed);
if (changed.ExternalIDs != null || resetOld) if (changed.ExternalIDs != null || resetOld)
{ {