Creating the provider composite and testing the merger

This commit is contained in:
Zoe Roux 2021-07-15 18:13:55 +02:00
parent 2d45d6422d
commit 0265c27010
6 changed files with 504 additions and 74 deletions

View File

@ -53,4 +53,18 @@ namespace Kyoo.Controllers
Task<ICollection<PeopleRole>> GetPeople(Show show);
}
/// <summary>
/// A special <see cref="IMetadataProvider"/> that merge results.
/// This interface exists to specify witch provider to use but it can be used like any other metadata provider.
/// </summary>
public interface IProviderComposite : IMetadataProvider
{
/// <summary>
/// Select witch providers to use.
/// The <see cref="IMetadataProvider"/> associated with the given <see cref="Provider"/> will be used.
/// </summary>
/// <param name="providers">The list of providers to use</param>
void UseProviders(IEnumerable<Provider> providers);
}
}

View File

@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Models;
using Kyoo.Models.Attributes;
namespace Kyoo
@ -111,7 +112,8 @@ namespace Kyoo
/// <param name="second">Missing fields of first will be completed by fields of this item. If second is null, the function no-op.</param>
/// <typeparam name="T">Fields of T will be merged</typeparam>
/// <returns><see cref="first"/></returns>
public static T Merge<T>(T first, T second)
[ContractAnnotation("=> null; first:notnull => notnull; second:notnull => notnull")]
public static T Merge<T>([CanBeNull] T first, [CanBeNull] T second)
{
if (first == null)
return second;
@ -127,9 +129,7 @@ namespace Kyoo
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
object defaultValue = property.PropertyType.GetClrDefault();
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
@ -139,11 +139,14 @@ namespace Kyoo
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
.GenericTypeArguments
.First();
Func<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
? (x, y) => x.Slug == y.Slug
: null;
property.SetValue(first, Utility.RunGenericMethod<object>(
typeof(Utility),
typeof(Merger),
nameof(MergeLists),
enumerableType,
oldValue, newValue, null));
oldValue, newValue, equalityComparer));
}
}

View File

@ -182,15 +182,49 @@ namespace Kyoo
return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] args)
/// <summary>
/// Retrieve a method from an <see cref="Type"/> with the given name and respect the
/// amount of parameters and generic parameters. This works for polymorphic methods.
/// </summary>
/// <param name="type">
/// The type owning the method. For non static methods, this is the <c>this</c>.
/// </param>
/// <param name="flag">
/// The binding flags of the method. This allow you to specify public/private and so on.
/// </param>
/// <param name="name">
/// The name of the method.
/// </param>
/// <param name="generics">
/// The list of generic parameters.
/// </param>
/// <param name="args">
/// The list of parameters.
/// </param>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The method handle of the matching method.</returns>
[PublicAPI]
[NotNull]
public static MethodInfo GetMethod([NotNull] Type type,
BindingFlags flag,
string name,
[NotNull] Type[] generics,
[NotNull] object[] args)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (generics == null)
throw new ArgumentNullException(nameof(generics));
if (args == null)
throw new ArgumentNullException(nameof(args));
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
.Where(x => x.GetParameters().Length == args.Length)
.IfEmpty(() => throw new NullReferenceException($"A method named {name} with " +
$"{args.Length} arguments and {generics.Length} generic " +
$"types could not be found on {type.Name}."))
.IfEmpty(() => throw new ArgumentException($"A method named {name} with " +
$"{args.Length} arguments and {generics.Length} generic " +
$"types could not be found on {type.Name}."))
// TODO this won't work but I don't know why.
// .Where(x =>
// {
@ -211,9 +245,34 @@ namespace Kyoo
if (methods.Length == 1)
return methods[0];
throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints.");
throw new ArgumentException($"Multiple methods named {name} match the generics and parameters constraints.");
}
/// <summary>
/// Run a generic static method for a runtime <see cref="Type"/>.
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="type">The generic type to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
@ -223,6 +282,34 @@ namespace Kyoo
return RunGenericMethod<T>(owner, methodName, new[] {type}, args);
}
/// <summary>
/// Run a generic static method for a multiple runtime <see cref="Type"/>.
/// If your generic method only needs one type, see
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="types">The list of generic types to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
[PublicAPI]
public static T RunGenericMethod<T>(
[NotNull] Type owner,
[NotNull] string methodName,
@ -238,9 +325,34 @@ namespace Kyoo
if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
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.ToArray());
}
/// <summary>
/// Run a generic method for a runtime <see cref="Type"/>.
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="instance">The <c>this</c> of the method to run.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="type">The generic type to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
@ -250,6 +362,33 @@ namespace Kyoo
return RunGenericMethod<T>(instance, methodName, new[] {type}, args);
}
/// <summary>
/// Run a generic method for a multiple runtime <see cref="Type"/>.
/// If your generic method only needs one type, see
/// <see cref="RunGenericMethod{T}(object,string,System.Type,object[])"/>
/// </summary>
/// <example>
/// To run <see cref="Merger.MergeLists{T}"/> for a List where you don't know the type at compile type,
/// you could do:
/// <code>
/// Utility.RunGenericMethod&lt;object&gt;(
/// typeof(Utility),
/// nameof(MergeLists),
/// enumerableType,
/// oldValue, newValue, equalityComparer)
/// </code>
/// </example>
/// <param name="instance">The <c>this</c> of the method to run.</param>
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
/// <param name="types">The list of generic types to run the method with.</param>
/// <param name="args">The list of arguments of the method</param>
/// <typeparam name="T">
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
/// </typeparam>
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
public static T RunGenericMethod<T>(
[NotNull] object instance,
[NotNull] string methodName,
@ -263,7 +402,7 @@ namespace Kyoo
if (types == null || types.Length == 0)
throw new ArgumentNullException(nameof(types));
MethodInfo method = GetMethod(instance.GetType(), BindingFlags.Instance, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(instance, args?.ToArray());
return (T)method.MakeGenericMethod(types).Invoke(instance, args.ToArray());
}
public static string ToQueryString(this Dictionary<string, string> query)

View File

@ -1,4 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Kyoo.Models;
using Kyoo.Models.Attributes;
using Xunit;
namespace Kyoo.Tests
@ -17,5 +21,192 @@ namespace Kyoo.Tests
Assert.Null(genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
public void MergeTest()
{
Genre genre = new()
{
ID = 5
};
Genre genre2 = new()
{
Name = "test"
};
Genre ret = Merger.Merge(genre, genre2);
Assert.True(ReferenceEquals(genre, ret));
Assert.Equal(5, ret.ID);
Assert.Equal("test", genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
[SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")]
public void MergeNullTests()
{
Genre genre = new()
{
ID = 5
};
Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null)));
Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre)));
Assert.Null(Merger.Merge<Genre>(null, null));
}
private class TestIOnMerge : IOnMerge
{
public void OnMerge(object other)
{
Exception exception = new();
exception.Data[0] = other;
throw exception;
}
}
[Fact]
public void OnMergeTest()
{
TestIOnMerge test = new();
TestIOnMerge test2 = new();
Assert.Throws<Exception>(() => Merger.Merge(test, test2));
try
{
Merger.Merge(test, test2);
}
catch (Exception ex)
{
Assert.True(ReferenceEquals(test2, ex.Data[0]));
}
}
private class Test
{
public int ID { get; set; }
public int[] Numbers { get; set; }
}
[Fact]
public void GlobalMergeListTest()
{
Test test = new()
{
ID = 5,
Numbers = new [] { 1 }
};
Test test2 = new()
{
Numbers = new [] { 3 }
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(3, ret.Numbers[1]);
}
[Fact]
public void GlobalMergeListDuplicatesTest()
{
Test test = new()
{
ID = 5,
Numbers = new [] { 1 }
};
Test test2 = new()
{
Numbers = new []
{
1,
3,
3
}
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(4, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(1, ret.Numbers[1]);
Assert.Equal(3, ret.Numbers[2]);
Assert.Equal(3, ret.Numbers[3]);
}
[Fact]
public void GlobalMergeListDuplicatesResourcesTest()
{
Show test = new()
{
ID = 5,
Genres = new [] { new Genre("test") }
};
Show test2 = new()
{
Genres = new []
{
new Genre("test"),
new Genre("test2")
}
};
Show ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Genres.Count);
Assert.Equal("test", ret.Genres.ToArray()[0].Slug);
Assert.Equal("test2", ret.Genres.ToArray()[1].Slug);
}
[Fact]
public void MergeListTest()
{
int[] first = { 1 };
int[] second = {
3,
3
};
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(3, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(3, ret[1]);
Assert.Equal(3, ret[2]);
}
[Fact]
public void MergeListDuplicateTest()
{
int[] first = { 1 };
int[] second = {
1,
3,
3
};
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(4, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(1, ret[1]);
Assert.Equal(3, ret[2]);
Assert.Equal(3, ret[3]);
}
[Fact]
public void MergeListDuplicateCustomEqualityTest()
{
int[] first = { 1 };
int[] second = {
3,
2
};
int[] ret = Merger.MergeLists(first, second, (x, y) => x % 2 == y % 2);
Assert.Equal(2, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(2, ret[1]);
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using Kyoo.Models;
using Xunit;
@ -31,5 +32,47 @@ namespace Kyoo.Tests
Assert.Equal("ID", Utility.GetPropertyName(memberCast));
Assert.Throws<ArgumentException>(() => Utility.GetPropertyName(null));
}
[Fact]
public void GetMethodTest()
{
MethodInfo method = Utility.GetMethod(typeof(UtilityTests),
BindingFlags.Instance | BindingFlags.Public,
nameof(GetMethodTest),
Array.Empty<Type>(),
Array.Empty<object>());
Assert.Equal(MethodBase.GetCurrentMethod(), method);
}
[Fact]
public void GetMethodInvalidGenericsTest()
{
Assert.Throws<ArgumentException>(() => Utility.GetMethod(typeof(UtilityTests),
BindingFlags.Instance | BindingFlags.Public,
nameof(GetMethodTest),
new [] { typeof(Utility) },
Array.Empty<object>()));
}
[Fact]
public void GetMethodInvalidParamsTest()
{
Assert.Throws<ArgumentException>(() => Utility.GetMethod(typeof(UtilityTests),
BindingFlags.Instance | BindingFlags.Public,
nameof(GetMethodTest),
Array.Empty<Type>(),
new object[] { this }));
}
[Fact]
public void GetMethodTest2()
{
MethodInfo method = Utility.GetMethod(typeof(Merger),
BindingFlags.Static | BindingFlags.Public,
nameof(Merger.MergeLists),
new [] { typeof(string) },
new object[] { "string", "string2", null });
Assert.Equal(nameof(Merger.MergeLists), method.Name);
}
}
}

View File

@ -1,29 +1,119 @@
using System;
using Kyoo.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Kyoo.Controllers
{
public class ProviderComposite : IMetadataProvider
/// <summary>
/// A metadata provider composite that merge results from all available providers.
/// </summary>
public class ProviderComposite : IProviderComposite
{
private readonly IEnumerable<IMetadataProvider> _providers;
/// <summary>
/// The list of metadata providers
/// </summary>
private readonly ICollection<IMetadataProvider> _providers;
/// <summary>
/// The list of selected providers. If no provider has been selected, this is null.
/// </summary>
private ICollection<Provider> _selectedProviders;
/// <summary>
/// The logger used to print errors.
/// </summary>
private readonly ILogger<ProviderComposite> _logger;
/// <summary>
/// Since this is a composite and not a real provider, no metadata is available.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// </summary>
public Provider Provider => null;
public ProviderComposite(IEnumerable<IMetadataProvider> providers)
/// <summary>
/// Create a new <see cref="ProviderComposite"/> with a list of available providers.
/// </summary>
/// <param name="providers">The list of providers to merge.</param>
/// <param name="logger">The logger used to print errors.</param>
public ProviderComposite(IEnumerable<IMetadataProvider> providers, ILogger<ProviderComposite> logger)
{
_providers = providers;
_providers = providers.ToArray();
_logger = logger;
}
public Provider Provider { get; }
public Task<T> Get<T>(T item) where T : class, IResource
/// <inheritdoc />
public void UseProviders(IEnumerable<Provider> providers)
{
throw new NotImplementedException();
_selectedProviders = providers.ToArray();
}
public Task<ICollection<T>> Search<T>(string query) where T : class, IResource
/// <summary>
/// Return the list of providers that should be used for queries.
/// </summary>
/// <returns>The list of providers to use, respecting the <see cref="UseProviders"/>.</returns>
private IEnumerable<IMetadataProvider> _GetProviders()
{
throw new NotImplementedException();
return _selectedProviders?
.Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
.Where(x => x != null)
?? _providers;
}
/// <inheritdoc />
public async Task<T> Get<T>(T item)
where T : class, IResource
{
T ret = null;
foreach (IMetadataProvider provider in _GetProviders())
{
try
{
ret = Merger.Merge(ret, await provider.Get(ret ?? item));
}
catch (NotSupportedException)
{
// Silenced
}
catch (Exception ex)
{
_logger.LogError(ex, "The provider {Provider} could not get a {Type}",
provider.Provider.Name, typeof(T).Name);
}
}
return Merger.Merge(ret, item);
}
/// <inheritdoc />
public async Task<ICollection<T>> Search<T>(string query)
where T : class, IResource
{
List<T> ret = new();
foreach (IMetadataProvider provider in _GetProviders())
{
try
{
ret.AddRange(await provider.Search<T>(query));
}
catch (NotSupportedException)
{
// Silenced
}
catch (Exception ex)
{
_logger.LogError(ex, "The provider {Provider} could not search for {Type}",
provider.Provider.Name, typeof(T).Name);
}
}
return ret;
}
public Task<ICollection<PeopleRole>> GetPeople(Show show)
@ -31,56 +121,6 @@ namespace Kyoo.Controllers
throw new NotImplementedException();
}
// private async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what)
// where T : new()
// {
// T ret = new();
//
// IEnumerable<IMetadataProvider> providers = library?.Providers
// .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
// .Where(x => x != null)
// ?? _providers;
//
// foreach (IMetadataProvider provider in providers)
// {
// try
// {
// ret = Merger.Merge(ret, await providerCall(provider));
// } catch (Exception ex)
// {
// await Console.Error.WriteLineAsync(
// $"The provider {provider.Provider.Name} could not work for {what}. Exception: {ex.Message}");
// }
// }
// return ret;
// }
//
// private async Task<List<T>> GetMetadata<T>(
// Func<IMetadataProvider, Task<ICollection<T>>> providerCall,
// Library library,
// string what)
// {
// List<T> ret = new();
//
// IEnumerable<IMetadataProvider> providers = library?.Providers
// .Select(x => _providers.FirstOrDefault(y => y.Provider.Slug == x.Slug))
// .Where(x => x != null)
// ?? _providers;
//
// foreach (IMetadataProvider provider in providers)
// {
// try
// {
// ret.AddRange(await providerCall(provider) ?? new List<T>());
// } catch (Exception ex)
// {
// await Console.Error.WriteLineAsync(
// $"The provider {provider.Provider.Name} coudln't work for {what}. Exception: {ex.Message}");
// }
// }
// return ret;
// }
//
// public async Task<Collection> GetCollectionFromName(string name, Library library)
// {
// Collection collection = await GetMetadata(