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; 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. Usfull 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 -, Uniformise accents é -> e) /// /// The string to slugify /// The slugified 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; } /// /// Set the image of a show using the type. /// /// The owner of the image /// The url of the image /// The type of the image public static void SetImage(Show show, string imgUrl, ImageType type) { switch(type) { case ImageType.Poster: show.Poster = imgUrl; break; case ImageType.Logo: show.Logo = imgUrl; break; case ImageType.Background: show.Backdrop = imgUrl; break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } /// /// Merge two lists, can keep duplicates or remove them. /// /// The first enumarble to merge /// The second enumerable to merge, if items from this list are equals to one from the first, they are not keeped /// 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 containg 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 genric 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}>"; } } }