diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs deleted file mode 100644 index 743cf545..00000000 --- a/Kyoo.Common/Utility.cs +++ /dev/null @@ -1,703 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Kyoo.Models.Attributes; - -namespace Kyoo -{ - /// - /// A set of utility functions that can be used everywhere. - /// - public static class Utility - { - /// - /// Is the lambda expression a member (like x => x.Body). - /// - /// The expression that should be checked - /// True if the expression is a member, false otherwise - public static bool IsPropertyExpression(LambdaExpression ex) - { - return ex == null || - ex.Body is MemberExpression || - ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; - } - - /// - /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) - /// - /// The expression - /// The name of the expression - /// If the expression is not a property, ArgumentException is thrown. - public static string GetPropertyName(LambdaExpression ex) - { - if (!IsPropertyExpression(ex)) - throw new ArgumentException($"{ex} is not a property expression."); - MemberExpression member = ex.Body.NodeType == ExpressionType.Convert - ? ((UnaryExpression)ex.Body).Operand as MemberExpression - : ex.Body as MemberExpression; - return member!.Member.Name; - } - - /// - /// Get the value of a member (property or field) - /// - /// The member value - /// The owner of this member - /// The value boxed as an object - /// if or is null. - /// The member is not a field or a property. - public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj) - { - if (member == null) - throw new ArgumentNullException(nameof(member)); - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - return member switch - { - PropertyInfo property => property.GetValue(obj), - FieldInfo field => field.GetValue(obj), - _ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).") - }; - } - - /// - /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) - /// - /// The string to slugify - /// The slug version of the given string - public static string ToSlug(string str) - { - if (str == null) - return null; - - str = str.ToLowerInvariant(); - - string normalizedString = str.Normalize(NormalizationForm.FormD); - StringBuilder stringBuilder = new(); - foreach (char c in normalizedString) - { - UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - if (unicodeCategory != UnicodeCategory.NonSpacingMark) - stringBuilder.Append(c); - } - str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); - - str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); - str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); - str = str.Trim('-', '_'); - str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); - return str; - } - - /// - /// 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 - public static T[] MergeLists(IEnumerable first, - IEnumerable second, - 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(); - } - - /// - /// 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 default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. - /// At the end, the OnMerge method of first will be called if first is a - /// - /// 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, 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.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) - property.SetValue(first, value); - } - - if (first is IOnMerge merge) - merge.OnMerge(second); - return first; - } - - /// - /// An advanced function. - /// This will set missing values of to the corresponding values of . - /// Enumerable will be merged (concatenated). - /// At the end, the OnMerge method of first will be called if first is a . - /// - /// The object to complete - /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. - /// Fields of T will be merged - /// - public static T Merge(T first, T second) - { - 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); - - foreach (PropertyInfo property in properties) - { - object oldValue = property.GetValue(first); - object newValue = property.GetValue(second); - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - - if (oldValue?.Equals(defaultValue) != false) - property.SetValue(first, newValue); - else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) - && property.PropertyType != typeof(string)) - { - property.SetValue(first, RunGenericMethod( - typeof(Utility), - nameof(MergeLists), - GetEnumerableType(property.PropertyType), - oldValue, newValue, null)); - } - } - - 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) - continue; - - object defaultValue = property.PropertyType.IsValueType - ? Activator.CreateInstance(property.PropertyType) - : null; - property.SetValue(obj, defaultValue); - } - - return obj; - } - - /// - /// Return every in the inheritance tree of the parameter (interfaces are not returned) - /// - /// The starting type - /// A list of types - /// can't be null - public static IEnumerable GetInheritanceTree([NotNull] this Type type) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - for (; type != null; type = type.BaseType) - yield return type; - } - - /// - /// Check if inherit from a generic type . - /// - /// Does this object's type is a - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) - { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - return IsOfGenericType(obj.GetType(), genericType); - } - - /// - /// Check if inherit from a generic type . - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// True if obj inherit from genericType. False otherwise - /// obj and genericType can't be null - public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// Get the generic definition of . - /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> - /// - /// The type to check - /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). - /// The generic definition of genericType that type inherit or null if type does not implement the generic type. - /// and can't be null - /// must be a generic type - public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (genericType == null) - throw new ArgumentNullException(nameof(genericType)); - if (!genericType.IsGenericType) - throw new ArgumentException($"{nameof(genericType)} is not a generic type."); - - IEnumerable types = genericType.IsInterface - ? type.GetInterfaces() - : type.GetInheritanceTree(); - return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); - } - - /// - /// A Select where the index of the item can be used. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped. - /// mapper can't be null - public static IEnumerable Map([CanBeNull] this IEnumerable self, - [NotNull] Func mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - - while (enumerator.MoveNext()) - { - yield return mapper(enumerator.Current, index); - index++; - } - } - - /// - /// A map where the mapping function is asynchronous. - /// Note: might interest you. - /// - /// The IEnumerable to map. If self is null, an empty list is returned - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - int index = 0; - - while (enumerator.MoveNext()) - { - yield return await mapper(enumerator.Current, index); - index++; - } - } - - /// - /// An asynchronous version of Select. - /// - /// The IEnumerable to map - /// The asynchronous function that will map each items - /// The type of items in - /// The type of items in the returned list - /// The list mapped as an AsyncEnumerable - /// mapper can't be null - public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, - [NotNull] Func> mapper) - { - if (self == null) - yield break; - if (mapper == null) - throw new ArgumentNullException(nameof(mapper)); - - using IEnumerator enumerator = self.GetEnumerator(); - - while (enumerator.MoveNext()) - yield return await mapper(enumerator.Current); - } - - /// - /// Convert an AsyncEnumerable to a List by waiting for every item. - /// - /// The async list - /// The type of items in the async list and in the returned list. - /// A task that will return a simple list - /// The list can't be null - public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) - { - if (self == null) - throw new ArgumentNullException(nameof(self)); - - List ret = new(); - - await foreach(T i in self) - ret.Add(i); - return ret; - } - - /// - /// If the enumerable is empty, execute an action. - /// - /// The enumerable to check - /// The action to execute is the list is empty - /// The type of items inside the list - /// - public static IEnumerable IfEmpty(this IEnumerable self, Action action) - { - using IEnumerator enumerator = self.GetEnumerator(); - - if (!enumerator.MoveNext()) - { - action(); - yield break; - } - - do - { - yield return enumerator.Current; - } - while (enumerator.MoveNext()); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - /// The type of items in the list - public static void ForEach([CanBeNull] this IEnumerable self, Action action) - { - if (self == null) - return; - foreach (T i in self) - action(i); - } - - /// - /// A foreach used as a function with a little specificity: the list can be null. - /// - /// The list to enumerate. If this is null, the function result in a no-op - /// The action to execute for each arguments - public static void ForEach([CanBeNull] this IEnumerable self, Action action) - { - if (self == null) - return; - foreach (object i in self) - action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) - { - if (self == null) - return; - foreach (T i in self) - await action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) - { - if (self == null) - return; - await foreach (T i in self) - action(i); - } - - public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) - { - if (self == null) - return; - foreach (object i in self) - await action(i); - } - - public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] 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}.")) - // TODO this won't work but I don't know why. - // .Where(x => - // { - // int i = 0; - // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) - - // TODO this won't work for Type because T is specified in arguments but not in the parameters type. - // .Where(x => - // { - // int i = 0; - // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); - // }) - // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) - .Take(2) - .ToArray(); - - if (methods.Length == 1) - return methods[0]; - throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints."); - } - - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(owner, methodName, new[] {type}, args); - } - - public static T RunGenericMethod( - [NotNull] Type owner, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) - { - if (owner == null) - throw new ArgumentNullException(nameof(owner)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - if (types == null) - throw new ArgumentNullException(nameof(types)); - 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()); - } - - public static T RunGenericMethod( - [NotNull] object instance, - [NotNull] string methodName, - [NotNull] Type type, - params object[] args) - { - return RunGenericMethod(instance, methodName, new[] {type}, args); - } - - public static T RunGenericMethod( - [NotNull] object instance, - [NotNull] string methodName, - [NotNull] Type[] types, - params object[] args) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); - if (methodName == null) - throw new ArgumentNullException(nameof(methodName)); - 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()); - } - - [NotNull] - public static Type GetEnumerableType([NoEnumeration] [NotNull] IEnumerable list) - { - if (list == null) - throw new ArgumentNullException(nameof(list)); - Type type = list.GetType().GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? list.GetType(); - return type.GetGenericArguments().First(); - } - - public static Type GetEnumerableType([NotNull] Type listType) - { - if (listType == null) - throw new ArgumentNullException(nameof(listType)); - if (!typeof(IEnumerable).IsAssignableFrom(listType)) - throw new InvalidOperationException($"The {nameof(listType)} parameter was not an IEnumerable."); - Type type = listType.GetInterfaces().FirstOrDefault(t => typeof(IEnumerable).IsAssignableFrom(t) - && t.GetGenericArguments().Any()) ?? listType; - return type.GetGenericArguments().First(); - } - - public static IEnumerable> BatchBy(this List list, int countPerList) - { - for (int i = 0; i < list.Count; i += countPerList) - yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); - } - - public static IEnumerable BatchBy(this IEnumerable list, int countPerList) - { - T[] ret = new T[countPerList]; - int i = 0; - - using IEnumerator enumerator = list.GetEnumerator(); - while (enumerator.MoveNext()) - { - ret[i] = enumerator.Current; - i++; - if (i < countPerList) - continue; - i = 0; - yield return ret; - } - - Array.Resize(ref ret, i); - yield return ret; - } - - public static string ToQueryString(this Dictionary query) - { - if (!query.Any()) - return string.Empty; - return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); - } - - [System.Diagnostics.CodeAnalysis.DoesNotReturn] - public static void ReThrow([NotNull] this Exception ex) - { - if (ex == null) - throw new ArgumentNullException(nameof(ex)); - ExceptionDispatchInfo.Capture(ex).Throw(); - } - - public static Task Then(this Task task, Action map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - map(x.Result); - return x.Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Map(this Task task, Func map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return map(x.Result); - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Cast(this Task task) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return (T)((dynamic)x).Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - /// - /// Get a friendly type name (supporting generics) - /// For example a list of string will be displayed as List<string> and not as List`1. - /// - /// The type to use - /// The friendly name of the type - public static string FriendlyName(this Type type) - { - if (!type.IsGenericType) - return type.Name; - string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); - return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; - } - } -} \ No newline at end of file diff --git a/Kyoo.Common/Utility/EnumerableExtensions.cs b/Kyoo.Common/Utility/EnumerableExtensions.cs new file mode 100644 index 00000000..e4d0379e --- /dev/null +++ b/Kyoo.Common/Utility/EnumerableExtensions.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of extensions class for enumerable. + /// + public static class EnumerableExtensions + { + /// + /// A Select where the index of the item can be used. + /// + /// The IEnumerable to map. If self is null, an empty list is returned + /// The function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped or null if the input map was null. + /// mapper can't be null + public static IEnumerable Map([CanBeNull] this IEnumerable self, + [NotNull] Func mapper) + { + if (self == null) + return null; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + static IEnumerable Generator(IEnumerable self, Func mapper) + { + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return mapper(enumerator.Current, index); + index++; + } + } + return Generator(self, mapper); + } + + /// + /// A map where the mapping function is asynchronous. + /// Note: might interest you. + /// + /// The IEnumerable to map. If self is null, an empty list is returned + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// mapper can't be null + public static async IAsyncEnumerable MapAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + int index = 0; + + while (enumerator.MoveNext()) + { + yield return await mapper(enumerator.Current, index); + index++; + } + } + + /// + /// An asynchronous version of Select. + /// + /// The IEnumerable to map + /// The asynchronous function that will map each items + /// The type of items in + /// The type of items in the returned list + /// The list mapped as an AsyncEnumerable + /// mapper can't be null + public static async IAsyncEnumerable SelectAsync([CanBeNull] this IEnumerable self, + [NotNull] Func> mapper) + { + if (self == null) + yield break; + if (mapper == null) + throw new ArgumentNullException(nameof(mapper)); + + using IEnumerator enumerator = self.GetEnumerator(); + + while (enumerator.MoveNext()) + yield return await mapper(enumerator.Current); + } + + /// + /// Convert an AsyncEnumerable to a List by waiting for every item. + /// + /// The async list + /// The type of items in the async list and in the returned list. + /// A task that will return a simple list + /// The list can't be null + public static async Task> ToListAsync([NotNull] this IAsyncEnumerable self) + { + if (self == null) + throw new ArgumentNullException(nameof(self)); + + List ret = new(); + + await foreach(T i in self) + ret.Add(i); + return ret; + } + + /// + /// If the enumerable is empty, execute an action. + /// + /// The enumerable to check + /// The action to execute is the list is empty + /// The type of items inside the list + /// + public static IEnumerable IfEmpty(this IEnumerable self, Action action) + { + using IEnumerator enumerator = self.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + action(); + yield break; + } + + do + { + yield return enumerator.Current; + } + while (enumerator.MoveNext()); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (T i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static void ForEach([CanBeNull] this IEnumerable self, Action action) + { + if (self == null) + return; + foreach (object i in self) + action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (object i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The list to enumerate. If this is null, the function result in a no-op + /// The asynchronous action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IEnumerable self, Func action) + { + if (self == null) + return; + foreach (T i in self) + await action(i); + } + + /// + /// A foreach used as a function with a little specificity: the list can be null. + /// + /// The async list to enumerate. If this is null, the function result in a no-op + /// The action to execute for each arguments + /// The type of items in the list. + public static async Task ForEachAsync([CanBeNull] this IAsyncEnumerable self, Action action) + { + if (self == null) + return; + await foreach (T i in self) + action(i); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + public static IEnumerable> BatchBy(this List list, int countPerList) + { + for (int i = 0; i < list.Count; i += countPerList) + yield return list.GetRange(i, Math.Min(list.Count - i, countPerList)); + } + + /// + /// Split a list in a small chunk of data. + /// + /// The list to split + /// The number of items in each chunk + /// The type of data in the initial list. + /// A list of chunks + public static IEnumerable BatchBy(this IEnumerable list, int countPerList) + { + T[] ret = new T[countPerList]; + int i = 0; + + using IEnumerator enumerator = list.GetEnumerator(); + while (enumerator.MoveNext()) + { + ret[i] = enumerator.Current; + i++; + if (i < countPerList) + continue; + i = 0; + yield return ret; + } + + Array.Resize(ref ret, i); + yield return ret; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Merger.cs b/Kyoo.Common/Utility/Merger.cs new file mode 100644 index 00000000..047b5b09 --- /dev/null +++ b/Kyoo.Common/Utility/Merger.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; +using Kyoo.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 + public static T[] MergeLists(IEnumerable first, + IEnumerable second, + 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(); + } + + /// + /// 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 default values of first to the value of second. ex: {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}. + /// At the end, the OnMerge method of first will be called if first is a + /// + /// 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, 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.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + + if (value?.Equals(defaultValue) == false && value != property.GetValue(first)) + property.SetValue(first, value); + } + + if (first is IOnMerge merge) + merge.OnMerge(second); + return first; + } + + /// + /// An advanced function. + /// This will set missing values of to the corresponding values of . + /// Enumerable will be merged (concatenated). + /// At the end, the OnMerge method of first will be called if first is a . + /// + /// The object to complete + /// Missing fields of first will be completed by fields of this item. If second is null, the function no-op. + /// Fields of T will be merged + /// + public static T Merge(T first, T second) + { + 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); + + foreach (PropertyInfo property in properties) + { + object oldValue = property.GetValue(first); + object newValue = property.GetValue(second); + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + + if (oldValue?.Equals(defaultValue) != false) + property.SetValue(first, newValue); + else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType) + && property.PropertyType != typeof(string)) + { + Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>)) + .GenericTypeArguments + .First(); + property.SetValue(first, Utility.RunGenericMethod( + typeof(Utility), + nameof(MergeLists), + enumerableType, + oldValue, newValue, null)); + } + } + + 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) + continue; + + object defaultValue = property.PropertyType.IsValueType + ? Activator.CreateInstance(property.PropertyType) + : null; + property.SetValue(obj, defaultValue); + } + + return obj; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs new file mode 100644 index 00000000..e25b595b --- /dev/null +++ b/Kyoo.Common/Utility/Utility.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A set of utility functions that can be used everywhere. + /// + public static class Utility + { + /// + /// Is the lambda expression a member (like x => x.Body). + /// + /// The expression that should be checked + /// True if the expression is a member, false otherwise + public static bool IsPropertyExpression(LambdaExpression ex) + { + if (ex == null) + return false; + return ex.Body is MemberExpression || + ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression; + } + + /// + /// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows) + /// + /// The expression + /// The name of the expression + /// If the expression is not a property, ArgumentException is thrown. + public static string GetPropertyName(LambdaExpression ex) + { + if (!IsPropertyExpression(ex)) + throw new ArgumentException($"{ex} is not a property expression."); + MemberExpression member = ex.Body.NodeType == ExpressionType.Convert + ? ((UnaryExpression)ex.Body).Operand as MemberExpression + : ex.Body as MemberExpression; + return member!.Member.Name; + } + + /// + /// Get the value of a member (property or field) + /// + /// The member value + /// The owner of this member + /// The value boxed as an object + /// if or is null. + /// The member is not a field or a property. + public static object GetValue([NotNull] this MemberInfo member, [NotNull] object obj) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return member switch + { + PropertyInfo property => property.GetValue(obj), + FieldInfo field => field.GetValue(obj), + _ => throw new ArgumentException($"Can't get value of a non property/field (member: {member}).") + }; + } + + /// + /// Slugify a string (Replace spaces by -, Uniformize accents é -> e) + /// + /// The string to slugify + /// The slug version of the given string + public static string ToSlug(string str) + { + if (str == null) + return null; + + str = str.ToLowerInvariant(); + + string normalizedString = str.Normalize(NormalizationForm.FormD); + StringBuilder stringBuilder = new(); + foreach (char c in normalizedString) + { + UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + stringBuilder.Append(c); + } + str = stringBuilder.ToString().Normalize(NormalizationForm.FormC); + + str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled); + str = Regex.Replace(str, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); + str = str.Trim('-', '_'); + str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled); + return str; + } + + /// + /// Return every in the inheritance tree of the parameter (interfaces are not returned) + /// + /// The starting type + /// A list of types + /// can't be null + public static IEnumerable GetInheritanceTree([NotNull] this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + for (; type != null; type = type.BaseType) + yield return type; + } + + /// + /// Check if inherit from a generic type . + /// + /// Does this object's type is a + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] object obj, [NotNull] Type genericType) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + return IsOfGenericType(obj.GetType(), genericType); + } + + /// + /// Check if inherit from a generic type . + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// True if obj inherit from genericType. False otherwise + /// obj and genericType can't be null + public static bool IsOfGenericType([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + /// + /// Get the generic definition of . + /// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string> + /// + /// The type to check + /// The generic type to check against (Only generic types are supported like typeof(IEnumerable<>). + /// The generic definition of genericType that type inherit or null if type does not implement the generic type. + /// and can't be null + /// must be a generic type + public static Type GetGenericDefinition([NotNull] Type type, [NotNull] Type genericType) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + if (!genericType.IsGenericType) + throw new ArgumentException($"{nameof(genericType)} is not a generic type."); + + IEnumerable types = genericType.IsInterface + ? type.GetInterfaces() + : type.GetInheritanceTree(); + return types.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType); + } + + public static MethodInfo GetMethod(Type type, BindingFlags flag, string name, Type[] generics, object[] 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}.")) + // TODO this won't work but I don't know why. + // .Where(x => + // { + // int i = 0; + // return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++])); + // }) + // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified.")) + + // TODO this won't work for Type because T is specified in arguments but not in the parameters type. + // .Where(x => + // { + // int i = 0; + // return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++])); + // }) + // .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types.")) + .Take(2) + .ToArray(); + + if (methods.Length == 1) + return methods[0]; + throw new NullReferenceException($"Multiple methods named {name} match the generics and parameters constraints."); + } + + public static T RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(owner, methodName, new[] {type}, args); + } + + public static T RunGenericMethod( + [NotNull] Type owner, + [NotNull] string methodName, + [NotNull] Type[] types, + params object[] args) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + if (types == null) + throw new ArgumentNullException(nameof(types)); + 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()); + } + + public static T RunGenericMethod( + [NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type type, + params object[] args) + { + return RunGenericMethod(instance, methodName, new[] {type}, args); + } + + public static T RunGenericMethod( + [NotNull] object instance, + [NotNull] string methodName, + [NotNull] Type[] types, + params object[] args) + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + 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()); + } + + public static string ToQueryString(this Dictionary query) + { + if (!query.Any()) + return string.Empty; + return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); + } + + [System.Diagnostics.CodeAnalysis.DoesNotReturn] + public static void ReThrow([NotNull] this Exception ex) + { + if (ex == null) + throw new ArgumentNullException(nameof(ex)); + ExceptionDispatchInfo.Capture(ex).Throw(); + } + + public static Task Then(this Task task, Action map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + map(x.Result); + return x.Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + public static Task Map(this Task task, Func map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return map(x.Result); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + public static Task Cast(this Task task) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return (T)((dynamic)x).Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Get a friendly type name (supporting generics) + /// For example a list of string will be displayed as List<string> and not as List`1. + /// + /// The type to use + /// The friendly name of the type + public static string FriendlyName(this Type type) + { + if (!type.IsGenericType) + return type.Name; + string generics = string.Join(", ", type.GetGenericArguments().Select(x => x.FriendlyName())); + return $"{type.Name[..type.Name.IndexOf('`')]}<{generics}>"; + } + } +} \ No newline at end of file diff --git a/Kyoo.CommonAPI/LocalRepository.cs b/Kyoo.CommonAPI/LocalRepository.cs index 7498fc14..d63c1b06 100644 --- a/Kyoo.CommonAPI/LocalRepository.cs +++ b/Kyoo.CommonAPI/LocalRepository.cs @@ -225,8 +225,8 @@ namespace Kyoo.Controllers T old = await GetWithTracking(edited.ID); if (resetOld) - Utility.Nullify(old); - Utility.Complete(old, edited, x => x.GetCustomAttribute() == null); + Merger.Nullify(old); + Merger.Complete(old, edited, x => x.GetCustomAttribute() == null); await EditRelations(old, edited, resetOld); await Database.SaveChangesAsync(); return old; diff --git a/Kyoo.Tests/UtilityTests.cs b/Kyoo.Tests/Utility/UtilityTests.cs similarity index 56% rename from Kyoo.Tests/UtilityTests.cs rename to Kyoo.Tests/Utility/UtilityTests.cs index e046cdb9..15469411 100644 --- a/Kyoo.Tests/UtilityTests.cs +++ b/Kyoo.Tests/Utility/UtilityTests.cs @@ -13,12 +13,23 @@ namespace Kyoo.Tests Expression> member = x => x.ID; Expression> memberCast = x => x.ID; - Assert.True(Utility.IsPropertyExpression(null)); + Assert.False(Utility.IsPropertyExpression(null)); Assert.True(Utility.IsPropertyExpression(member)); Assert.True(Utility.IsPropertyExpression(memberCast)); Expression> call = x => x.GetID("test"); Assert.False(Utility.IsPropertyExpression(call)); } + + [Fact] + public void GetPropertyName_Test() + { + Expression> member = x => x.ID; + Expression> memberCast = x => x.ID; + + Assert.Equal("ID", Utility.GetPropertyName(member)); + Assert.Equal("ID", Utility.GetPropertyName(memberCast)); + Assert.Throws(() => Utility.GetPropertyName(null)); + } } } \ No newline at end of file diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs index 61f41593..4a0a0cd1 100644 --- a/Kyoo/Controllers/ProviderManager.cs +++ b/Kyoo/Controllers/ProviderManager.cs @@ -29,7 +29,7 @@ namespace Kyoo.Controllers { try { - ret = Utility.Merge(ret, await providerCall(provider)); + ret = Merger.Merge(ret, await providerCall(provider)); } catch (Exception ex) { await Console.Error.WriteLineAsync( diff --git a/Kyoo/Controllers/Repositories/EpisodeRepository.cs b/Kyoo/Controllers/Repositories/EpisodeRepository.cs index 8452b950..6487e6b1 100644 --- a/Kyoo/Controllers/Repositories/EpisodeRepository.cs +++ b/Kyoo/Controllers/Repositories/EpisodeRepository.cs @@ -213,7 +213,7 @@ namespace Kyoo.Controllers /// Set track's index and ensure that every tracks is well-formed. /// /// The resource to fix. - /// The parameter is returnned. + /// The parameter is returned. private async Task ValidateTracks(Episode resource) { resource.Tracks = await resource.Tracks.MapAsync((x, i) =>