CodingStyle: Fixing more issues

This commit is contained in:
Zoe Roux 2021-09-03 22:28:35 +02:00
parent d3a03771dd
commit aec2a8f51a
16 changed files with 390 additions and 270 deletions

View File

@ -10,7 +10,7 @@ using Kyoo.Abstractions.Models.Exceptions;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// An interface to interract with the database. Every repository is mapped through here.
/// An interface to interact with the database. Every repository is mapped through here.
/// </summary>
public interface ILibraryManager
{
@ -20,7 +20,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type you want</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The repository corresponding</returns>
IRepository<T> GetRepository<T>() where T : class, IResource;
IRepository<T> GetRepository<T>()
where T : class, IResource;
/// <summary>
/// The repository that handle libraries.
@ -28,7 +29,7 @@ namespace Kyoo.Abstractions.Controllers
ILibraryRepository LibraryRepository { get; }
/// <summary>
/// The repository that handle libraries's items (a wrapper arround shows & collections).
/// The repository that handle libraries items (a wrapper around shows & collections).
/// </summary>
ILibraryItemRepository LibraryItemRepository { get; }
@ -90,7 +91,8 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get<T>(int id) where T : class, IResource;
Task<T> Get<T>(int id)
where T : class, IResource;
/// <summary>
/// Get the resource by it's slug
@ -100,7 +102,8 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource found</returns>
[ItemNotNull]
Task<T> Get<T>(string slug) where T : class, IResource;
Task<T> Get<T>(string slug)
where T : class, IResource;
/// <summary>
/// Get the resource by a filter function.
@ -110,7 +113,8 @@ namespace Kyoo.Abstractions.Controllers
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The first resource found that match the where function</returns>
[ItemNotNull]
Task<T> Get<T>(Expression<Func<T, bool>> where) where T : class, IResource;
Task<T> Get<T>(Expression<Func<T, bool>> where)
where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber
@ -161,7 +165,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
[ItemCanBeNull]
Task<T> GetOrDefault<T>(int id) where T : class, IResource;
Task<T> GetOrDefault<T>(int id)
where T : class, IResource;
/// <summary>
/// Get the resource by it's slug or null if it is not found.
@ -170,7 +175,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The resource found</returns>
[ItemCanBeNull]
Task<T> GetOrDefault<T>(string slug) where T : class, IResource;
Task<T> GetOrDefault<T>(string slug)
where T : class, IResource;
/// <summary>
/// Get the resource by a filter function or null if it is not found.
@ -179,7 +185,8 @@ namespace Kyoo.Abstractions.Controllers
/// <typeparam name="T">The type of the resource</typeparam>
/// <returns>The first resource found that match the where function</returns>
[ItemCanBeNull]
Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where) where T : class, IResource;
Task<T> GetOrDefault<T>(Expression<Func<T, bool>> where)
where T : class, IResource;
/// <summary>
/// Get a season from it's showID and it's seasonNumber or null if it is not found.
@ -219,20 +226,19 @@ namespace Kyoo.Abstractions.Controllers
[ItemCanBeNull]
Task<Episode> GetOrDefault(string showSlug, int seasonNumber, int episodeNumber);
/// <summary>
/// Load a related resource
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <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}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
@ -244,13 +250,13 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <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}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
@ -262,7 +268,7 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns>
@ -273,24 +279,25 @@ namespace Kyoo.Abstractions.Controllers
where T : class, IResource;
/// <summary>
/// Load a related resource without specifing it's type.
/// Load a related resource without specifying it's type.
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load{T}(T, string, bool)"/>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Load([NotNull] IResource obj, string memberName, bool force = false);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(int id,
@ -299,7 +306,7 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="id">The ID of the library</param>
/// <param name="where">A filter function</param>
@ -313,11 +320,11 @@ namespace Kyoo.Abstractions.Controllers
) => GetItemsFromLibrary(id, where, new Sort<LibraryItem>(sort), limit);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<LibraryItem>> GetItemsFromLibrary(string slug,
@ -326,7 +333,7 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.
/// Get items (A wrapper around shows or collections) from a library.
/// </summary>
/// <param name="slug">The slug of the library</param>
/// <param name="where">A filter function</param>
@ -339,13 +346,12 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default
) => GetItemsFromLibrary(slug, where, new Sort<LibraryItem>(sort), limit);
/// <summary>
/// Get people's roles from a show.
/// </summary>
/// <param name="showID">The ID of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(int showID,
@ -372,7 +378,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
/// <param name="showSlug">The slug of the show</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetPeopleFromShow(string showSlug,
@ -394,13 +400,12 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default
) => GetPeopleFromShow(showSlug, where, new Sort<PeopleRole>(sort), limit);
/// <summary>
/// Get people's roles from a person.
/// </summary>
/// <param name="id">The id of the person</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(int id,
@ -427,7 +432,7 @@ namespace Kyoo.Abstractions.Controllers
/// </summary>
/// <param name="slug">The slug of the person</param>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <returns>A list of items that match every filters</returns>
Task<ICollection<PeopleRole>> GetRolesFromPeople(string slug,
@ -449,13 +454,13 @@ namespace Kyoo.Abstractions.Controllers
Pagination limit = default
) => GetRolesFromPeople(slug, where, new Sort<PeopleRole>(sort), limit);
/// <summary>
/// Setup relations between a show, a library and a collection
/// </summary>
/// <param name="showID">The show's ID to setup relations with</param>
/// <param name="libraryID">The library's ID to setup relations with (optional)</param>
/// <param name="collectionID">The collection's ID to setup relations with (optional)</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task AddShowLink(int showID, int? libraryID, int? collectionID);
/// <summary>
@ -464,19 +469,21 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="show">The show to setup relations with</param>
/// <param name="library">The library to setup relations with (optional)</param>
/// <param name="collection">The collection to setup relations with (optional)</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task AddShowLink([NotNull] Show show, Library library, Collection collection);
/// <summary>
/// Get all resources with filters
/// </summary>
/// <param name="where">A filter function</param>
/// <param name="sort">Sort informations (sort order & sort by)</param>
/// <param name="sort">Sort information (sort order & sort by)</param>
/// <param name="limit">How many items to return and where to start</param>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>(Expression<Func<T, bool>> where = null,
Sort<T> sort = default,
Pagination limit = default) where T : class, IResource;
Pagination limit = default)
where T : class, IResource;
/// <summary>
/// Get all resources with filters
@ -488,7 +495,8 @@ namespace Kyoo.Abstractions.Controllers
/// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll<T>([Optional] Expression<Func<T, bool>> where,
Expression<Func<T, object>> sort,
Pagination limit = default) where T : class, IResource
Pagination limit = default)
where T : class, IResource
{
return GetAll(where, new Sort<T>(sort), limit);
}
@ -499,7 +507,8 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="where">A filter function</param>
/// <typeparam name="T">The type of resources to load</typeparam>
/// <returns>A list of resources that match every filters</returns>
Task<int> GetCount<T>(Expression<Func<T, bool>> where = null) where T : class, IResource;
Task<int> GetCount<T>(Expression<Func<T, bool>> where = null)
where T : class, IResource;
/// <summary>
/// Search for a resource
@ -507,15 +516,17 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="query">The search query</param>
/// <typeparam name="T">The type of resources</typeparam>
/// <returns>A list of 20 items that match the search query</returns>
Task<ICollection<T>> Search<T>(string query) where T : class, IResource;
Task<ICollection<T>> Search<T>(string query)
where T : class, IResource;
/// <summary>
/// Create a new resource.
/// </summary>
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The resource registers and completed by database's informations (related items & so on)</returns>
Task<T> Create<T>([NotNull] T item) where T : class, IResource;
/// <returns>The resource registers and completed by database's information (related items & so on)</returns>
Task<T> Create<T>([NotNull] T item)
where T : class, IResource;
/// <summary>
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
@ -523,17 +534,19 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item">The item to register</param>
/// <typeparam name="T">The type of resource</typeparam>
/// <returns>The newly created item or the existing value if it existed.</returns>
Task<T> CreateIfNotExists<T>([NotNull] T item) where T : class, IResource;
Task<T> CreateIfNotExists<T>([NotNull] T item)
where T : class, IResource;
/// <summary>
/// Edit a resource
/// </summary>
/// <param name="item">The resourcce to edit, it's ID can't change.</param>
/// <param name="item">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's informations (related items & so on)</returns>
Task<T> Edit<T>(T item, bool resetOld) where T : class, IResource;
/// <returns>The resource edited and completed by database's information (related items & so on)</returns>
Task<T> Edit<T>(T item, bool resetOld)
where T : class, IResource;
/// <summary>
/// Delete a resource.
@ -541,7 +554,9 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="item">The resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(T item) where T : class, IResource;
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(T item)
where T : class, IResource;
/// <summary>
/// Delete a resource by it's ID.
@ -549,7 +564,9 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="id">The id of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(int id) where T : class, IResource;
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(int id)
where T : class, IResource;
/// <summary>
/// Delete a resource by it's slug.
@ -557,6 +574,8 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="slug">The slug of the resource to delete</param>
/// <typeparam name="T">The type of resource to delete</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
Task Delete<T>(string slug) where T : class, IResource;
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task Delete<T>(string slug)
where T : class, IResource;
}
}

View File

@ -1,7 +1,7 @@
using Kyoo.Abstractions.Models;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
namespace Kyoo.Abstractions.Controllers
{
@ -30,6 +30,7 @@ namespace Kyoo.Abstractions.Controllers
/// Merging metadata is the job of Kyoo, a complex <typeparamref name="T"/> is given
/// to make a precise search and give you every available properties, not to discard properties.
/// </remarks>
/// <typeparam name="T">The type of resource to retrieve metadata for.</typeparam>
/// <returns>A new <typeparamref name="T"/> containing metadata from your provider or null</returns>
[ItemCanBeNull]
Task<T> Get<T>([NotNull] T item)
@ -39,6 +40,7 @@ namespace Kyoo.Abstractions.Controllers
/// Search for a specific type of items with a given query.
/// </summary>
/// <param name="query">The search query to use.</param>
/// <typeparam name="T">The type of resource to search metadata for.</typeparam>
/// <returns>The list of items that could be found on this specific provider.</returns>
[ItemNotNull]
Task<ICollection<T>> Search<T>(string query)
@ -62,7 +64,7 @@ namespace Kyoo.Abstractions.Controllers
/// <summary>
/// Since this is a composite and not a real provider, no metadata is available.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// It is not meant to be stored or selected. This class will handle merge based on what is required.
/// </summary>
public Provider Provider => null;

View File

@ -0,0 +1,29 @@
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Kyoo.Abstractions.Controllers
{
/// <summary>
/// A service to validate permissions.
/// </summary>
public interface IPermissionValidator
{
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">The permission attribute to validate.</param>
/// <returns>An authorization filter used to validate the permission.</returns>
IFilterMetadata Create(PermissionAttribute attribute);
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
/// </param>
/// <returns>An authorization filter used to validate the permission.</returns>
IFilterMetadata Create(PartialPermissionAttribute attribute);
}
}

View File

@ -35,7 +35,7 @@ namespace Kyoo.Abstractions.Controllers
/// <c>true</c> if the plugin should be enabled, <c>false</c> otherwise.
/// If a plugin is not enabled, no configure method will be called.
/// This allow one to enable a plugin if a specific configuration value is set or if the environment contains
/// the right settings.
/// the right settings.
/// </summary>
/// <remarks>
/// By default, a plugin is always enabled. This method can be overriden to change this behavior.

View File

@ -19,8 +19,9 @@ namespace Kyoo.Abstractions.Controllers
/// The count of items to return.
/// </summary>
public int Count { get; }
/// <summary>
/// Where to start? Using the given sort
/// Where to start? Using the given sort.
/// </summary>
public int AfterID { get; }
@ -53,6 +54,7 @@ namespace Kyoo.Abstractions.Controllers
/// The sort key. This member will be used to sort the results.
/// </summary>
public Expression<Func<T, object>> Key { get; }
/// <summary>
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
/// </summary>
@ -121,7 +123,8 @@ namespace Kyoo.Abstractions.Controllers
/// A common repository for every resources.
/// </summary>
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
public interface IRepository<T> : IBaseRepository where T : class, IResource
public interface IRepository<T> : IBaseRepository
where T : class, IResource
{
/// <summary>
/// Get a resource from it's ID.

View File

@ -0,0 +1,70 @@
using System;
using Kyoo.Abstractions.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Abstractions.Models.Permissions
{
/// <summary>
/// Specify one part of a permissions needed for the API (the kind or the type).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PartialPermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission type.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
public PartialPermissionAttribute(string type)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
}
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="permission">The kind of permission needed.</param>
public PartialPermissionAttribute(Kind permission)
{
Kind = permission;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
}
}

View File

@ -0,0 +1,110 @@
using System;
using Kyoo.Abstractions.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Abstractions.Models.Permissions
{
/// <summary>
/// The kind of permission needed.
/// </summary>
public enum Kind
{
/// <summary>
/// Allow the user to read for this kind of data.
/// </summary>
Read,
/// <summary>
/// Allow the user to write for this kind of data.
/// </summary>
Write,
/// <summary>
/// Allow the user to create this kind of data.
/// </summary>
Create,
/// <summary>
/// Allow the user to delete this kind od data.
/// </summary>
Delete
}
/// <summary>
/// The group of the permission.
/// </summary>
public enum Group
{
/// <summary>
/// Allow all operations on basic items types.
/// </summary>
Overall,
/// <summary>
/// Allow operation on sensitive items like libraries path, configurations and so on.
/// </summary>
Admin
}
/// <summary>
/// Specify permissions needed for the API.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission as string.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// The group of this permission.
/// </summary>
public Group Group { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="permission">The kind of permission needed.</param>
/// <param name="group">
/// The group of this permission (allow grouped permission like overall.read
/// for all read permissions of this group).
/// </param>
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
Kind = permission;
Group = group;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
/// <summary>
/// Return this permission attribute as a string.
/// </summary>
/// <returns>The string representation.</returns>
public string AsPermissionString()
{
return Type;
}
}
}

View File

@ -1,172 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Kyoo.Abstractions.Models.Permissions
{
/// <summary>
/// The kind of permission needed.
/// </summary>
public enum Kind
{
Read,
Write,
Create,
Delete
}
/// <summary>
/// The group of the permission.
/// </summary>
public enum Group
{
Overall,
Admin
}
/// <summary>
/// Specify permissions needed for the API.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission as string.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// The group of this permission
/// </summary>
public Group Group { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
/// <param name="permission">The kind of permission needed</param>
/// <param name="group">
/// The group of this permission (allow grouped permission like overall.read
/// for all read permissions of this group)
/// </param>
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
Kind = permission;
Group = group;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
/// <summary>
/// Return this permission attribute as a string
/// </summary>
/// <returns>The string representation.</returns>
public string AsPermissionString()
{
return Type;
}
}
/// <summary>
/// Specify one part of a permissions needed for the API (the kind or the type).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PartialPermissionAttribute : Attribute, IFilterFactory
{
/// <summary>
/// The needed permission type.
/// </summary>
public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="type">
/// The type of the action
/// (if the type ends with api, it will be removed. This allow you to use nameof(YourApi)).
/// </param>
public PartialPermissionAttribute(string type)
{
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3];
Type = type.ToLower();
}
/// <summary>
/// Ask a permission to run an action.
/// </summary>
/// <remarks>
/// With this attribute, you can only specify a type or a kind.
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
/// lead to unspecified behaviors.
/// </remarks>
/// <param name="permission">The kind of permission needed</param>
public PartialPermissionAttribute(Kind permission)
{
Kind = permission;
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
}
/// <inheritdoc />
public bool IsReusable => true;
}
/// <summary>
/// A service to validate permissions
/// </summary>
public interface IPermissionValidator
{
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">The permission attribute to validate</param>
/// <returns>An authorization filter used to validate the permission</returns>
IFilterMetadata Create(PermissionAttribute attribute);
/// <summary>
/// Create an IAuthorizationFilter that will be used to validate permissions.
/// This can registered with any lifetime.
/// </summary>
/// <param name="attribute">
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
/// </param>
/// <returns>An authorization filter used to validate the permission</returns>
IFilterMetadata Create(PartialPermissionAttribute attribute);
}
}

View File

@ -50,7 +50,7 @@ namespace Kyoo.Abstractions.Models
public string TrailerUrl => Images?.GetValueOrDefault(Models.Images.Trailer);
/// <summary>
/// The date this show started airing. It can be null if this is unknown.
/// The date this show started airing. It can be null if this is unknown.
/// </summary>
public DateTime? StartAir { get; set; }
@ -103,6 +103,7 @@ namespace Kyoo.Abstractions.Models
/// The ID of the Studio that made this show.
/// </summary>
[SerializeIgnore] public int? StudioID { get; set; }
/// <summary>
/// The Studio that made this show.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
@ -145,19 +146,48 @@ namespace Kyoo.Abstractions.Models
public void OnMerge(object merged)
{
if (People != null)
{
foreach (PeopleRole link in People)
link.Show = this;
}
if (Seasons != null)
{
foreach (Season season in Seasons)
season.Show = this;
}
if (Episodes != null)
{
foreach (Episode episode in Episodes)
episode.Show = this;
}
}
}
/// <summary>
/// The enum containing show's status.
/// </summary>
public enum Status { Unknown, Finished, Airing, Planned }
public enum Status
{
/// <summary>
/// The status of the show is not known.
/// </summary>
Unknown,
/// <summary>
/// The show has finished airing.
/// </summary>
Finished,
/// <summary>
/// The show is still actively airing.
/// </summary>
Airing,
/// <summary>
/// This show has not aired yet but has been announced.
/// </summary>
Planned
}
}

View File

@ -85,7 +85,7 @@ namespace Kyoo.Authentication
/// <inheritdoc />
public void Configure(ContainerBuilder builder)
{
builder.RegisterType<PermissionValidatorFactory>().As<IPermissionValidator>().SingleInstance();
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
DefaultCorsPolicyService cors = new(_logger)
{

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Authentication.Models;
using Microsoft.AspNetCore.Authentication;
@ -17,7 +18,7 @@ namespace Kyoo.Authentication
/// A permission validator to validate permission with user Permission array
/// or the default array from the configurations if the user is not logged.
/// </summary>
public class PermissionValidatorFactory : IPermissionValidator
public class PermissionValidator : IPermissionValidator
{
/// <summary>
/// The permissions options to retrieve default permissions.
@ -25,10 +26,10 @@ namespace Kyoo.Authentication
private readonly IOptionsMonitor<PermissionOption> _options;
/// <summary>
/// Create a new factory with the given options
/// Create a new factory with the given options.
/// </summary>
/// <param name="options">The option containing default values.</param>
public PermissionValidatorFactory(IOptionsMonitor<PermissionOption> options)
public PermissionValidator(IOptionsMonitor<PermissionOption> options)
{
_options = options;
}
@ -36,46 +37,49 @@ namespace Kyoo.Authentication
/// <inheritdoc />
public IFilterMetadata Create(PermissionAttribute attribute)
{
return new PermissionValidator(attribute.Type, attribute.Kind, attribute.Group, _options);
return new PermissionValidatorFilter(attribute.Type, attribute.Kind, attribute.Group, _options);
}
/// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute)
{
return new PermissionValidator((object)attribute.Type ?? attribute.Kind, _options);
return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, _options);
}
/// <summary>
/// The authorization filter used by <see cref="PermissionValidatorFactory"/>
/// The authorization filter used by <see cref="PermissionValidator"/>.
/// </summary>
private class PermissionValidator : IAsyncAuthorizationFilter
private class PermissionValidatorFilter : IAsyncAuthorizationFilter
{
/// <summary>
/// The permission to validate
/// The permission to validate.
/// </summary>
private readonly string _permission;
/// <summary>
/// The kind of permission needed
/// The kind of permission needed.
/// </summary>
private readonly Kind? _kind;
/// <summary>
/// The group of he permission
/// The group of he permission.
/// </summary>
private readonly Group _group = Group.Overall;
/// <summary>
/// The permissions options to retrieve default permissions.
/// </summary>
private readonly IOptionsMonitor<PermissionOption> _options;
/// <summary>
/// Create a new permission validator with the given options
/// Create a new permission validator with the given options.
/// </summary>
/// <param name="permission">The permission to validate</param>
/// <param name="kind">The kind of permission needed</param>
/// <param name="group">The group of the permission</param>
/// <param name="permission">The permission to validate.</param>
/// <param name="kind">The kind of permission needed.</param>
/// <param name="group">The group of the permission.</param>
/// <param name="options">The option containing default values.</param>
public PermissionValidator(string permission, Kind kind, Group group, IOptionsMonitor<PermissionOption> options)
public PermissionValidatorFilter(string permission, Kind kind, Group group,
IOptionsMonitor<PermissionOption> options)
{
_permission = permission;
_kind = kind;
@ -84,11 +88,11 @@ namespace Kyoo.Authentication
}
/// <summary>
/// Create a new permission validator with the given options
/// Create a new permission validator with the given options.
/// </summary>
/// <param name="partialInfo">The partial permission to validate</param>
/// <param name="partialInfo">The partial permission to validate.</param>
/// <param name="options">The option containing default values.</param>
public PermissionValidator(object partialInfo, IOptionsMonitor<PermissionOption> options)
public PermissionValidatorFilter(object partialInfo, IOptionsMonitor<PermissionOption> options)
{
if (partialInfo is Kind kind)
_kind = kind;
@ -99,7 +103,6 @@ namespace Kyoo.Authentication
_options = options;
}
/// <inheritdoc />
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
@ -127,8 +130,10 @@ namespace Kyoo.Authentication
"are not supported.");
}
if (permission == null || kind == null)
{
throw new ArgumentException("The permission type or kind is still missing after two partial " +
"permission attributes, this is unsupported.");
}
}
string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}";

View File

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Permissions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
@ -9,7 +11,8 @@ namespace Kyoo.Core.Controllers
/// </summary>
public class PassthroughPermissionValidator : IPermissionValidator
{
// ReSharper disable once SuggestBaseTypeForParameter
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor",
Justification = "ILogger should include the typeparam for context.")]
public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger)
{
logger.LogWarning("No permission validator has been enabled, all users will have all permissions");
@ -28,8 +31,8 @@ namespace Kyoo.Core.Controllers
}
/// <summary>
/// An useless filter that does nothing.
/// An useless filter that does nothing.
/// </summary>
private class PassthroughValidator : IFilterMetadata { }
}
}
}

View File

@ -41,9 +41,9 @@ namespace Kyoo.Core.Api
}
// TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)]
[HttpGet("{slug}")]
[HttpGet("direct/{slug}")]
// TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)]
public async Task<IActionResult> Direct(string slug)
{
try
@ -114,4 +114,4 @@ namespace Kyoo.Core.Api
return PhysicalFile(path, "video/MP2T");
}
}
}
}

View File

@ -26,38 +26,47 @@ namespace Kyoo.Database
/// All libraries of Kyoo. See <see cref="Library"/>.
/// </summary>
public DbSet<Library> Libraries { get; set; }
/// <summary>
/// All collections of Kyoo. See <see cref="Collection"/>.
/// </summary>
public DbSet<Collection> Collections { get; set; }
/// <summary>
/// All shows of Kyoo. See <see cref="Show"/>.
/// </summary>
public DbSet<Show> Shows { get; set; }
/// <summary>
/// All seasons of Kyoo. See <see cref="Season"/>.
/// </summary>
public DbSet<Season> Seasons { get; set; }
/// <summary>
/// All episodes of Kyoo. See <see cref="Episode"/>.
/// </summary>
public DbSet<Episode> Episodes { get; set; }
/// <summary>
/// All tracks of Kyoo. See <see cref="Track"/>.
/// </summary>
public DbSet<Track> Tracks { get; set; }
/// <summary>
/// All genres of Kyoo. See <see cref="Genres"/>.
/// </summary>
public DbSet<Genre> Genres { get; set; }
/// <summary>
/// All people of Kyoo. See <see cref="People"/>.
/// </summary>
public DbSet<People> People { get; set; }
/// <summary>
/// All studios of Kyoo. See <see cref="Studio"/>.
/// </summary>
public DbSet<Studio> Studios { get; set; }
/// <summary>
/// All providers of Kyoo. See <see cref="Provider"/>.
/// </summary>
@ -74,12 +83,12 @@ namespace Kyoo.Database
public DbSet<PeopleRole> PeopleRoles { get; set; }
/// <summary>
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>
/// Episodes with a watch percentage. See <see cref="WatchedEpisode"/>.
/// </summary>
public DbSet<WatchedEpisode> WatchedEpisodes { get; set; }
/// <summary>
/// The list of library items (shows and collections that are part of a library - or the global one)
/// The list of library items (shows and collections that are part of a library - or the global one).
/// </summary>
/// <remarks>
/// This set is ready only, on most database this will be a view.
@ -471,7 +480,7 @@ namespace Kyoo.Database
/// <exception cref="DuplicatedItemException">A duplicated item has been found.</exception>
/// <returns>The number of state entries written to the database.</returns>
public async Task<int> SaveChangesAsync(string duplicateMessage,
CancellationToken cancellationToken = new())
CancellationToken cancellationToken = default)
{
try
{
@ -492,7 +501,7 @@ namespace Kyoo.Database
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete</param>
/// <returns>The number of state entries written to the database or -1 if a duplicate exist.</returns>
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = new())
public async Task<int> SaveIfNoDuplicates(CancellationToken cancellationToken = default)
{
try
{
@ -507,7 +516,7 @@ namespace Kyoo.Database
/// <summary>
/// Return the first resource with the given slug that is currently tracked by this context.
/// This allow one to limit redundant calls to <see cref="IRepository{T}.CreateIfNotExists"/> during the
/// same transaction and prevent fails from EF when two same entities are being tracked.
/// same transaction and prevent fails from EF when two same entities are being tracked.
/// </summary>
/// <param name="slug">The slug of the resource to check</param>
/// <typeparam name="T">The type of entity to check</typeparam>
@ -548,4 +557,4 @@ namespace Kyoo.Database
/// <returns>An expression representing the like query. It can directly be passed to a where call.</returns>
public abstract Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format);
}
}
}

View File

@ -23,6 +23,7 @@ namespace Kyoo.TheMovieDb
/// The API key used to authenticate with TheMovieDb API.
/// </summary>
private readonly IOptions<TheMovieDbOptions> _apiKey;
/// <summary>
/// The logger to use in ase of issue.
/// </summary>
@ -43,7 +44,7 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Create a new <see cref="TheMovieDbProvider"/> using the given api key.
/// </summary>
/// <param name="apiKey">The api key</param>
/// <param name="apiKey">The api key.</param>
/// <param name="logger">The logger to use in case of issue.</param>
public TheMovieDbProvider(IOptions<TheMovieDbOptions> apiKey, ILogger<TheMovieDbProvider> logger)
{
@ -51,7 +52,6 @@ namespace Kyoo.TheMovieDb
_logger = logger;
}
/// <inheritdoc />
public Task<T> Get<T>(T item)
where T : class, IResource
@ -71,8 +71,8 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Get a collection using it's id, if the id is not present in the collection, fallback to a name search.
/// </summary>
/// <param name="collection">The collection to search for</param>
/// <returns>A collection containing metadata from TheMovieDb</returns>
/// <param name="collection">The collection to search for.</param>
/// <returns>A collection containing metadata from TheMovieDb.</returns>
private async Task<Collection> _GetCollection(Collection collection)
{
if (!collection.TryGetID(Provider.Slug, out int id))
@ -89,8 +89,8 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Get a show using it's id, if the id is not present in the show, fallback to a title search.
/// </summary>
/// <param name="show">The show to search for</param>
/// <returns>A show containing metadata from TheMovieDb</returns>
/// <param name="show">The show to search for.</param>
/// <returns>A show containing metadata from TheMovieDb.</returns>
private async Task<Show> _GetShow(Show show)
{
if (!show.TryGetID(Provider.Slug, out int id))
@ -119,7 +119,7 @@ namespace Kyoo.TheMovieDb
/// Get a season using it's show and it's season number.
/// </summary>
/// <param name="season">The season to retrieve metadata for.</param>
/// <returns>A season containing metadata from TheMovieDb</returns>
/// <returns>A season containing metadata from TheMovieDb.</returns>
private async Task<Season> _GetSeason(Season season)
{
if (season.Show == null)
@ -139,10 +139,10 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Get an episode using it's show, it's season number and it's episode number.
/// Absolute numbering is not supported.
/// Absolute numbering is not supported.
/// </summary>
/// <param name="episode">The episode to retrieve metadata for.</param>
/// <returns>An episode containing metadata from TheMovieDb</returns>
/// <returns>An episode containing metadata from TheMovieDb.</returns>
private async Task<Episode> _GetEpisode(Episode episode)
{
if (episode.Show == null)
@ -163,8 +163,8 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Get a person using it's id, if the id is not present in the person, fallback to a name search.
/// </summary>
/// <param name="person">The person to search for</param>
/// <returns>A person containing metadata from TheMovieDb</returns>
/// <param name="person">The person to search for.</param>
/// <returns>A person containing metadata from TheMovieDb.</returns>
private async Task<People> _GetPerson(People person)
{
if (!person.TryGetID(Provider.Slug, out int id))
@ -181,8 +181,8 @@ namespace Kyoo.TheMovieDb
/// <summary>
/// Get a studio using it's id, if the id is not present in the studio, fallback to a name search.
/// </summary>
/// <param name="studio">The studio to search for</param>
/// <returns>A studio containing metadata from TheMovieDb</returns>
/// <param name="studio">The studio to search for.</param>
/// <returns>A studio containing metadata from TheMovieDb.</returns>
private async Task<Studio> _GetStudio(Studio studio)
{
if (!studio.TryGetID(Provider.Slug, out int id))
@ -277,4 +277,4 @@ namespace Kyoo.TheMovieDb
.ToArray();
}
}
}
}

View File

@ -3,6 +3,10 @@
<Rule Id="SA1413" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
<Rule Id="SA1414" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
<Rule Id="SA1201" Action="None" /> <!-- ElementsMustAppearInTheCorrectOrder -->
<Rule Id="SA1202" Action="None" /> <!-- ElementsMustBeOrderedByAccess -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
<Rule Id="SA1309" Action="None" /> <!-- FieldNamesMustNotBeginWithUnderscore -->
<Rule Id="SX1309" Action="Warning" /> <!-- FieldNamesMustBeginWithUnderscore -->
@ -11,13 +15,20 @@
<Rule Id="SA1101" Action="None" /> <!-- PrefixLocalCallsWithThis -->
<Rule Id="SX1101" Action="Warning" /> <!-- DoNotPrefixLocalMembersWithThis -->
<Rule Id="SA1134" Action="None" /> <!-- AttributesMustNotShareLine -->
<Rule Id="SA1117" Action="None" /> <!-- ParametersMustBeOnSameLineOrSeparateLines -->
<Rule Id="SA1116" Action="None" /> <!-- SplitParametersMustStartOnLineAfterDeclaration -->
<Rule Id="SA1111" Action="None" /> <!-- ClosingParenthesisMustBeOnLineOfLastParameter -->
<Rule Id="SA1009" Action="None" /> <!-- ClosingParenthesisMustBeSpacedCorrectly (bugged if the parenthesis is on a line by iteself) -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.SpacingRules">
<Rule Id="SA1502" Action="None"/> <!-- DocumentationLinesMustBeginWithSingleSpace -->
<Rule Id="SA1027" Action="None"/> <!-- UseTabsCorrectly (smarts tabs are broken). TODO find a way to enable smart tabs -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.LayoutRules">
<Rule Id="SA1503" Action="None"/> <!-- BracesMustNotBeOmitted -->
<Rule Id="SA1520" Action="None"/> <!-- UseBracesConsistently -->
<Rule Id="SA1515" Action="None"/> <!-- SingleLineCommentMustBePrecededByBlankLine -->
<Rule Id="SA1513" Action="None"/> <!-- ClosingBraceMustBeFollowedByBlankLine -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
<Rule Id="SA1642" Action="None" /> <!-- ConstructorSummaryDocumentationMustBeginWithStandardText -->
@ -27,5 +38,6 @@
<Rule Id="SA1633" Action="None" /> <!-- FileMustHaveHeader TODO remove this, this is only temporary -->
<Rule Id="SA1629" Action="None" /> <!-- DocumentationTextMustEndWithAPeriod TODO remove this, this is only temporary -->
</Rules>
</RuleSet>