Splitting task tests and adding documentation to resources

This commit is contained in:
Zoe Roux 2021-05-30 23:42:14 +02:00
parent d6630f29ea
commit 2bc559424c
17 changed files with 584 additions and 116 deletions

View File

@ -242,6 +242,9 @@ namespace Kyoo.Controllers
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new();
@ -254,6 +257,9 @@ namespace Kyoo.Controllers
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new();
@ -265,6 +271,9 @@ namespace Kyoo.Controllers
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T>([NotNull] T obj, string memberName)
where T : class, IResource;
@ -273,6 +282,9 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
Task Load([NotNull] IResource obj, string memberName);
/// <summary>

View File

@ -0,0 +1,13 @@
using System;
using JetBrains.Annotations;
using Kyoo.Models.Attributes;
namespace Kyoo.Common.Models.Attributes
{
/// <summary>
/// An attribute to mark Link properties on resource.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[MeansImplicitUse]
public class LinkAttribute : SerializeIgnoreAttribute { }
}

View File

@ -1,17 +1,34 @@
using System;
using Kyoo.Controllers;
namespace Kyoo.Models.Attributes
{
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
/// <summary>
/// The targeted relation can be edited via calls to the repository's <see cref="IRepository{T}.Edit"/> method.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class EditableRelationAttribute : Attribute { }
/// <summary>
/// The targeted relation can be loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class LoadableRelationAttribute : Attribute
{
/// <summary>
/// The name of the field containing the related resource's ID.
/// </summary>
public string RelationID { get; }
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary>
public LoadableRelationAttribute() {}
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
/// </summary>
/// <param name="relationID">The name of the RelationID field.</param>
public LoadableRelationAttribute(string relationID)
{
RelationID = relationID;

View File

@ -1,31 +1,59 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A class representing collections of <see cref="Show"/>.
/// A collection can also be stored in a <see cref="Library"/>.
/// </summary>
public class Collection : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this collection.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The description of this collection.
/// </summary>
public string Overview { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
/// <summary>
/// The list of shows contained in this collection.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of libraries that contains this collection.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Collection, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
#endif
public Collection() { }
public Collection(string slug, string name, string overview, string poster)
{
Slug = slug;
Name = name;
Overview = overview;
Poster = poster;
}
/// <summary>
/// The internal link between this collection and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Collection, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this collection and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> LibraryLinks { get; set; }
#endif
}
}

View File

@ -1,40 +1,126 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A class to represent a single show's episode.
/// This is also used internally for movies (their number is juste set to -1).
/// </summary>
public class Episode : IResource, IOnMerge
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
/// <summary>
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
/// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; }
/// <summary>
/// The ID of the Show containing this episode. This value is only set when the <see cref="Show"/> has been loaded.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
/// <summary>
/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
/// <summary>
/// The ID of the Season containing this episode. This value is only set when the <see cref="Season"/> has been loaded.
/// </summary>
[SerializeIgnore] public int? SeasonID { get; set; }
[LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; }
/// <summary>
/// The season that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// This can be null if the season is unknown and the episode is only identified by it's <see cref="AbsoluteNumber"/>.
/// </summary>
[LoadableRelation(nameof(SeasonID))] public Season Season { get; set; }
/// <summary>
/// The season in witch this episode is in. This defaults to -1 if not specified.
/// </summary>
public int SeasonNumber { get; set; } = -1;
/// <summary>
/// The number of this episode is it's season. This defaults to -1 if not specified.
/// </summary>
public int EpisodeNumber { get; set; } = -1;
/// <summary>
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
/// This defaults to -1 if not specified.
/// </summary>
public int AbsoluteNumber { get; set; } = -1;
/// <summary>
/// The path of the video file for this episode. Any format supported by a <see cref="IFileManager"/> is allowed.
/// </summary>
[SerializeIgnore] public string Path { get; set; }
/// <summary>
/// The path of this episode's thumbnail.
/// By default, the http path for the thumbnail is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; }
/// <summary>
/// The title of this episode.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The overview of this episode.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// The release date of this episode. It can be null if unknown.
/// </summary>
public DateTime? ReleaseDate { get; set; }
public int Runtime { get; set; } //This runtime variable should be in minutes
/// <summary>
/// The link to metadata providers that this episode has. See <see cref="MetadataID"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<Track> Tracks { get; set; }
/// <summary>
/// The list of tracks this episode has. This lists video, audio and subtitles available.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<Track> Tracks { get; set; }
public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber)
/// <summary>
/// Get the slug of an episode.
/// </summary>
/// <param name="showSlug">The slug of the show. It can't be null.</param>
/// <param name="seasonNumber">
/// The season in which the episode is.
/// If this is a movie or if the episode should be referred by it's absolute number, set this to -1.
/// </param>
/// <param name="episodeNumber">
/// The number of the episode in it's season.
/// If this is a movie or if the episode should be referred by it's absolute number, set this to -1.
/// </param>
/// <param name="absoluteNumber">
/// The absolute number of this show.
/// If you don't know it or this is a movie, use -1
/// </param>
/// <returns>The slug corresponding to the given arguments</returns>
/// <exception cref="ArgumentNullException">The given show slug was null.</exception>
public static string GetSlug([NotNull] string showSlug,
int seasonNumber = -1,
int episodeNumber = -1,
int absoluteNumber = -1)
{
if (showSlug == null)
throw new ArgumentException("Show's slug is null. Can't find episode's slug.");
throw new ArgumentNullException(nameof(showSlug));
return seasonNumber switch
{
-1 when absoluteNumber == -1 => showSlug,
@ -43,6 +129,7 @@ namespace Kyoo.Models
};
}
/// <inheritdoc />
public void OnMerge(object merged)
{
Episode other = (Episode)merged;

View File

@ -1,40 +1,51 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A genre that allow one to specify categories for shows.
/// </summary>
public class Genre : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this genre.
/// </summary>
public string Name { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of shows that have this genre.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Show, Genre>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this genre and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Show, Genre>> ShowLinks { get; set; }
#endif
/// <summary>
/// Create a new, empty <see cref="Genre"/>.
/// </summary>
public Genre() {}
/// <summary>
/// Create a new <see cref="Genre"/> and specify it's <see cref="Name"/>.
/// The <see cref="Slug"/> is automatically calculated from it's name.
/// </summary>
/// <param name="name">The name of this genre.</param>
public Genre(string name)
{
Slug = Utility.ToSlug(name);
Name = name;
}
public Genre(string slug, string name)
{
Slug = slug;
Name = name;
}
public Genre(int id, string slug, string name)
{
ID = id;
Slug = slug;
Name = name;
}
}
}

View File

@ -1,3 +1,5 @@
using Kyoo.Controllers;
namespace Kyoo.Models
{
/// <summary>
@ -8,6 +10,10 @@ namespace Kyoo.Models
/// <summary>
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
/// </summary>
/// <remarks>
/// You don't need to specify an ID manually when creating a new resource,
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
/// </remarks>
public int ID { get; set; }
/// <summary>

View File

@ -1,24 +1,60 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A library containing <see cref="Show"/> and <see cref="Collection"/>.
/// </summary>
public class Library : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this library.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The list of paths that this library is responsible for. This is mainly used by the Scan task.
/// </summary>
public string[] Paths { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<Provider> Providers { get; set; }
/// <summary>
/// The list of <see cref="Provider"/> used for items in this library.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<Provider> Providers { get; set; }
[LoadableRelation] public virtual ICollection<Show> Shows { get; set; }
[LoadableRelation] public virtual ICollection<Collection> Collections { get; set; }
/// <summary>
/// The list of shows in this library.
/// </summary>
[LoadableRelation] public ICollection<Show> Shows { get; set; }
/// <summary>
/// The list of collections in this library.
/// </summary>
[LoadableRelation] public ICollection<Collection> Collections { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Show>> ShowLinks { get; set; }
[SerializeIgnore] public virtual ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
/// <summary>
/// The internal link between this library and provider in the <see cref="Providers"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> ProviderLinks { get; set; }
/// <summary>
/// The internal link between this library and shows in the <see cref="Shows"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Show>> ShowLinks { get; set; }
/// <summary>
/// The internal link between this library and collection in the <see cref="Collections"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Collection>> CollectionLinks { get; set; }
#endif
}
}

View File

@ -3,14 +3,37 @@ using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
/// </summary>
public class People : IResource
{
/// <inheritdoc />
public int ID { get; set; }
public string Slug { get; set; }
public string Name { get; set; }
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<PeopleRole> Roles { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this person.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; }
/// <summary>
/// The link to metadata providers that this person has. See <see cref="MetadataID"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary>
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<PeopleRole> Roles { get; set; }
}
}

View File

@ -1,37 +1,72 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// This class contains metadata about <see cref="IMetadataProvider"/>.
/// You can have providers even if you don't have the corresponding <see cref="IMetadataProvider"/>.
/// </summary>
public class Provider : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug { get; set; }
/// <summary>
/// The name of this provider.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The path of this provider's logo.
/// By default, the http path for this logo is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; }
/// <summary>
/// The extension of the logo. This is used for http responses.
/// </summary>
[SerializeIgnore] public string LogoExtension { get; set; }
[LoadableRelation] public virtual ICollection<Library> Libraries { get; set; }
/// <summary>
/// The list of libraries that uses this provider.
/// </summary>
[LoadableRelation] public ICollection<Library> Libraries { get; set; }
#if ENABLE_INTERNAL_LINKS
[SerializeIgnore] public virtual ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
[SerializeIgnore] public virtual ICollection<MetadataID> MetadataLinks { get; set; }
/// <summary>
/// The internal link between this provider and libraries in the <see cref="Libraries"/> list.
/// </summary>
[Link] public ICollection<Link<Library, Provider>> LibraryLinks { get; set; }
/// <summary>
/// The internal link between this provider and related <see cref="MetadataID"/>.
/// </summary>
[Link] public ICollection<MetadataID> MetadataLinks { get; set; }
#endif
/// <summary>
/// Create a new, default, <see cref="Provider"/>
/// </summary>
public Provider() { }
/// <summary>
/// Create a new <see cref="Provider"/> and specify it's <see cref="Name"/>.
/// The <see cref="Slug"/> is automatically calculated from it's name.
/// </summary>
/// <param name="name">The name of this provider.</param>
/// <param name="logo">The logo of this provider.</param>
public Provider(string name, string logo)
{
Slug = Utility.ToSlug(name);
Name = name;
Logo = logo;
}
public Provider(int id, string name, string logo)
{
ID = id;
Slug = Utility.ToSlug(name);
Name = name;
Logo = logo;
}
}
}

View File

@ -1,25 +1,75 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Kyoo.Controllers;
using Kyoo.Models.Attributes;
namespace Kyoo.Models
{
/// <summary>
/// A season of a <see cref="Show"/>.
/// </summary>
public class Season : IResource
{
/// <inheritdoc />
public int ID { get; set; }
/// <inheritdoc />
public string Slug => $"{ShowSlug}-s{SeasonNumber}";
[SerializeIgnore] public int ShowID { get; set; }
/// <summary>
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
/// </summary>
[SerializeIgnore] public string ShowSlug { private get; set; }
[LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; }
/// <summary>
/// The ID of the Show containing this season. This value is only set when the <see cref="Show"/> has been loaded.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
/// <summary>
/// The show that contains this season. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show Show { get; set; }
/// <summary>
/// The number of this season. This can be set to 0 to indicate specials. This defaults to -1 for unset.
/// </summary>
public int SeasonNumber { get; set; } = -1;
/// <summary>
/// The title of this season.
/// </summary>
public string Title { get; set; }
/// <summary>
/// A quick overview of this season.
/// </summary>
public string Overview { get; set; }
public int? Year { get; set; }
/// <summary>
/// The starting air date of this season.
/// </summary>
public DateTime? StartDate { get; set; }
/// <summary>
/// The ending date of this season.
/// </summary>
public DateTime? EndDate { get; set; }
/// <summary>
/// The path of this poster.
/// By default, the http path for this poster is returned from the public API.
/// This can be disabled using the internal query flag.
/// </summary>
[SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; }
[EditableRelation] [LoadableRelation] public virtual ICollection<MetadataID> ExternalIDs { get; set; }
/// <summary>
/// The link to metadata providers that this episode has. See <see cref="MetadataID"/> for more information.
/// </summary>
[EditableRelation] [LoadableRelation] public ICollection<MetadataID> ExternalIDs { get; set; }
[LoadableRelation] public virtual ICollection<Episode> Episodes { get; set; }
/// <summary>
/// The list of episodes that this season contains.
/// </summary>
[LoadableRelation] public ICollection<Episode> Episodes { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Kyoo.Common.Models.Attributes;
namespace Kyoo.Models
{
@ -52,7 +53,7 @@ namespace Kyoo.Models
/// <summary>
/// Links between Users and Shows.
/// </summary>
public ICollection<Link<User, Show>> ShowLinks { get; set; }
[Link] public ICollection<Link<User, Show>> ShowLinks { get; set; }
#endif
}

View File

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo
{
/// <summary>
/// A class containing helper method for tasks.
/// </summary>
public static class TaskUtils
{
/// <summary>
/// Run a method after the execution of the task.
/// </summary>
/// <param name="task">The task to wait.</param>
/// <param name="then">
/// The method to run after the task finish. This will only be run if the task finished successfully.
/// </param>
/// <typeparam name="T">The type of the item in the task.</typeparam>
/// <returns>A continuation task wrapping the initial task and adding a continuation method.</returns>
/// <exception cref="TaskCanceledException"></exception>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<T> Then<T>(this Task<T> task, Action<T> then)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
then(x.Result);
return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// Map the result of a task to another result.
/// </summary>
/// <param name="task">The task to map.</param>
/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
/// <typeparam name="T">The type of returns of the given task</typeparam>
/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
return map(x.Result);
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// A method to return the a default value from a task if the initial task is null.
/// </summary>
/// <param name="value">The initial task</param>
/// <typeparam name="T">The type that the task will return</typeparam>
/// <returns>A non-null task.</returns>
[NotNull]
public static Task<T> DefaultIfNull<T>([CanBeNull] Task<T> value)
{
return value ?? Task.FromResult<T>(default);
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@ -270,43 +269,6 @@ namespace Kyoo
throw new ArgumentNullException(nameof(ex));
ExceptionDispatchInfo.Capture(ex).Throw();
}
public static Task<T> Then<T>(this Task<T> task, Action<T> 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<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> 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<T> Cast<T>(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);
}
/// <summary>
/// Get a friendly type name (supporting generics)

View File

@ -1,3 +1,4 @@
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Controllers;
using Kyoo.Models;
@ -15,20 +16,61 @@ namespace Kyoo.Tests.SpecificTests
{
_repository = Repositories.LibraryManager.ShowRepository;
}
//
[Fact]
public async Task EditTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Path = "/super";
value.Title = "New Title";
Show edited = await _repository.Edit(value, false);
KAssert.DeepEqual(value, edited);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows.FirstAsync();
KAssert.DeepEqual(show, value);
}
[Fact]
public async Task EditGenreTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Genres = new[] {new Genre("test")};
Show edited = await _repository.Edit(value, false);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name}));
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.Genres)
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name}));
}
// [Fact]
// public async Task EditTest()
// public async Task EditPeopleTest()
// {
// Show value = await _repository.Get(TestSample.Get<Show>().Slug);
// value.Path = "/super";
// value.Title = "New Title";
// value.People = new[] {new People
// {
// Name = "test"
// }};
// Show edited = await _repository.Edit(value, false);
// KAssert.DeepEqual(value, edited);
//
// Assert.Equal(value.Slug, edited.Slug);
// Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), edited.Genres.Select(x => new{x.Slug, x.Name}));
//
// await using DatabaseContext database = Repositories.Context.New();
// Show show = await database.Shows.FirstAsync();
// Show show = await database.Shows
// .Include(x => x.Genres)
// .FirstAsync();
//
// KAssert.DeepEqual(show, value);
// Assert.Equal(value.Slug, show.Slug);
// Assert.Equal(value.Genres.Select(x => new{x.Slug, x.Name}), show.Genres.Select(x => new{x.Slug, x.Name}));
// }
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Kyoo.Tests
{
public class TaskTests
{
[Fact]
public async Task DefaultIfNullTest()
{
Assert.Equal(0, await TaskUtils.DefaultIfNull<int>(null));
Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1)));
}
[Fact]
public async Task ThenTest()
{
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
.Then(_ => throw new ArgumentException()));
Assert.Equal(1, await Task.FromResult(1)
.Then(_ => {}));
static async Task<int> Faulted()
{
await Task.Delay(1);
throw new ArgumentException();
}
await Assert.ThrowsAsync<ArgumentException>(() => Faulted().Then(_ => KAssert.Fail()));
static async Task<int> Infinite()
{
await Task.Delay(100000);
return 1;
}
CancellationTokenSource token = new();
token.Cancel();
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Then(_ => {}));
}
[Fact]
public async Task MapTest()
{
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
.Map<int, int>(_ => throw new ArgumentException()));
Assert.Equal(2, await Task.FromResult(1)
.Map(x => x + 1));
static async Task<int> Faulted()
{
await Task.Delay(1);
throw new ArgumentException();
}
await Assert.ThrowsAsync<ArgumentException>(() => Faulted()
.Map(x =>
{
KAssert.Fail();
return x;
}));
static async Task<int> Infinite()
{
await Task.Delay(100000);
return 1;
}
CancellationTokenSource token = new();
token.Cancel();
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Map(x => x));
}
}
}

View File

@ -103,9 +103,9 @@ namespace Kyoo.Controllers
await base.Validate(resource);
if (resource.Studio != null)
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.Genres = await resource.Genres
.SelectAsync(x => _genres.CreateIfNotExists(x))
.ToListAsync();
resource.Genres = await TaskUtils.DefaultIfNull(resource.Genres
?.SelectAsync(x => _genres.CreateIfNotExists(x))
.ToListAsync());
resource.GenreLinks = resource.Genres?
.Select(x => Link.UCreate(resource, x))
.ToList();