API: Documenting the libraries'api

This commit is contained in:
Zoe Roux 2021-09-24 10:22:02 +02:00
parent 38ec742db6
commit 2287919b60
5 changed files with 137 additions and 135 deletions

View File

@ -39,6 +39,11 @@ namespace Kyoo.Abstractions.Models.Permissions
/// </summary> /// </summary>
public Kind Kind { get; } public Kind Kind { get; }
/// <summary>
/// The group of this permission.
/// </summary>
public Group Group { get; set; }
/// <summary> /// <summary>
/// Ask a permission to run an action. /// Ask a permission to run an action.
/// </summary> /// </summary>

View File

@ -61,7 +61,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute) public IFilterMetadata Create(PartialPermissionAttribute attribute)
{ {
return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, _options); return new PermissionValidatorFilter((object)attribute.Type ?? attribute.Kind, attribute.Group, _options);
} }
/// <summary> /// <summary>
@ -109,15 +109,24 @@ namespace Kyoo.Authentication
/// Create a new permission validator with the given options. /// Create a new permission validator with the given options.
/// </summary> /// </summary>
/// <param name="partialInfo">The partial permission to validate.</param> /// <param name="partialInfo">The partial permission to validate.</param>
/// <param name="group">The group of the permission.</param>
/// <param name="options">The option containing default values.</param> /// <param name="options">The option containing default values.</param>
public PermissionValidatorFilter(object partialInfo, IOptionsMonitor<PermissionOption> options) public PermissionValidatorFilter(object partialInfo, Group? group, IOptionsMonitor<PermissionOption> options)
{ {
if (partialInfo is Kind kind) switch (partialInfo)
_kind = kind; {
else if (partialInfo is string perm) case Kind kind:
_permission = perm; _kind = kind;
else break;
throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind."); case string perm:
_permission = perm;
break;
default:
throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind.");
}
if (group != null)
_group = group.Value;
_options = options; _options = options;
} }

View File

@ -133,7 +133,7 @@ namespace Kyoo.Core.Api
/// <param name="afterID">An optional track's ID to start the query from this specific item.</param> /// <param name="afterID">An optional track's ID to start the query from this specific item.</param>
/// <returns>A page of tracks.</returns> /// <returns>A page of tracks.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response> /// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No track with the given ID or slug could be found.</response> /// <response code="404">No episode with the given ID or slug could be found.</response>
/// TODO fix the /watch endpoint link (when operations ID are specified). /// TODO fix the /watch endpoint link (when operations ID are specified).
[HttpGet("{identifier:id}/tracks")] [HttpGet("{identifier:id}/tracks")]
[HttpGet("{identifier:id}/track", Order = AlternativeRoute)] [HttpGet("{identifier:id}/track", Order = AlternativeRoute)]

View File

@ -186,7 +186,7 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))]
public virtual async Task<ActionResult<T>> Create([FromBody] T resource) public async Task<ActionResult<T>> Create([FromBody] T resource)
{ {
try try
{ {

View File

@ -19,198 +19,186 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Core.Models.Options; using Kyoo.Core.Models.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api namespace Kyoo.Core.Api
{ {
[Route("api/library")] /// <summary>
/// Information about one or multiple <see cref="Library"/>.
/// </summary>
[Route("api/libraries")] [Route("api/libraries")]
[Route("api/library", Order = AlternativeRoute)]
[ApiController] [ApiController]
[PartialPermission(nameof(LibraryApi))] [PartialPermission(nameof(LibraryApi), Group = Group.Admin)]
[ApiDefinition("Library", Group = ResourcesGroup)]
public class LibraryApi : CrudApi<Library> public class LibraryApi : CrudApi<Library>
{ {
/// <summary>
/// The library manager used to modify or retrieve information in the data store.
/// </summary>
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
public LibraryApi(ILibraryManager libraryManager, ITaskManager taskManager, IOptions<BasicOptions> options) /// <summary>
/// Create a new <see cref="EpisodeApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information in the data store.
/// </param>
/// <param name="options">
/// Options used to retrieve the base URL of Kyoo.
/// </param>
public LibraryApi(ILibraryManager libraryManager, IOptions<BasicOptions> options)
: base(libraryManager.LibraryRepository, options.Value.PublicUrl) : base(libraryManager.LibraryRepository, options.Value.PublicUrl)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_taskManager = taskManager;
} }
[PartialPermission(Kind.Create)] /// <summary>
public override async Task<ActionResult<Library>> Create(Library resource) /// Get shows
{ /// </summary>
ActionResult<Library> result = await base.Create(resource); /// <remarks>
if (result.Value != null) /// List the shows that are part of this library.
{ /// </remarks>
_taskManager.StartTask("scan", /// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
new Progress<float>(), /// <param name="sortBy">A key to sort shows by.</param>
new Dictionary<string, object> { { "slug", result.Value.Slug } }); /// <param name="where">An optional list of filters.</param>
} /// <param name="limit">The number of shows to return.</param>
return result; /// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
} /// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
[HttpGet("{id:int}/show")] /// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{id:int}/shows")] [HttpGet("{identifier:id}/shows")]
[HttpGet("{identifier:id}/show", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(int id, [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50) [FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{ {
try try
{ {
ICollection<Show> resources = await _libraryManager.GetAll( ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.ID == id)), ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Library>(x => x.Libraries)),
new Sort<Show>(sortBy), new Sort<Show>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
} }
} }
[HttpGet("{slug}/show")] /// <summary>
[HttpGet("{slug}/shows")] /// Get collections
/// </summary>
/// <remarks>
/// List the collections that are part of this library.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of collections to return.</param>
/// <param name="afterID">An optional collection's ID to start the query from this specific item.</param>
/// <returns>A page of collections.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/collections")]
[HttpGet("{identifier:id}/collection", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Show>>> GetShows(string slug, [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 50,
{ [FromQuery] int? afterID = null)
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Libraries.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{id:int}/collection")]
[HttpGet("{id:int}/collections")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Collection>>> GetCollections(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetAll( ICollection<Collection> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.ID == id)), ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Library>(x => x.Libraries)),
new Sort<Collection>(sortBy), new Sort<Collection>(sortBy),
new Pagination(limit, afterID)); new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null) if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
} }
} }
[HttpGet("{slug}/collection")] /// <summary>
[HttpGet("{slug}/collections")] /// Get items
/// </summary>
/// <remarks>
/// List all items of this library.
/// An item can ether represent a collection or a show.
/// This endpoint allow one to retrieve all collections and shows that are not contained in a collection.
/// This is what is displayed on the /browse page of the webapp.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Library"/>.</param>
/// <param name="sortBy">A key to sort items by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of items to return.</param>
/// <param name="afterID">An optional item's ID to start the query from this specific item.</param>
/// <returns>A page of items.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No library with the given ID or slug could be found.</response>
[HttpGet("{identifier:id}/items")]
[HttpGet("{identifier:id}/item", Order = AlternativeRoute)]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<Collection>>> GetCollections(string slug, [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<LibraryItem>>> GetItems(Identifier identifier,
[FromQuery] string sortBy, [FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where, [FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20) [FromQuery] int limit = 50,
[FromQuery] int? afterID = null)
{ {
try try
{ {
ICollection<Collection> resources = await _libraryManager.GetAll( Expression<Func<LibraryItem, bool>> whereQuery = ApiHelper.ParseWhere<LibraryItem>(where);
ApiHelper.ParseWhere<Collection>(where, x => x.Libraries.Any(y => y.Slug == slug)), Sort<LibraryItem> sort = new(sortBy);
new Sort<Collection>(sortBy), Pagination pagination = new(limit, afterID);
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(slug) == null) ICollection<LibraryItem> resources = await identifier.Match(
id => _libraryManager.GetItemsFromLibrary(id, whereQuery, sort, pagination),
slug => _libraryManager.GetItemsFromLibrary(slug, whereQuery, sort, pagination)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Library>()) == null)
return NotFound(); return NotFound();
return Page(resources, limit); return Page(resources, limit);
} }
catch (ArgumentException ex) catch (ArgumentException ex)
{ {
return BadRequest(new { Error = ex.Message }); return BadRequest(new RequestError(ex.Message));
}
}
[HttpGet("{id:int}/item")]
[HttpGet("{id:int}/items")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<LibraryItem>>> GetItems(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(id,
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(id) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
[HttpGet("{slug}/item")]
[HttpGet("{slug}/items")]
[PartialPermission(Kind.Read)]
public async Task<ActionResult<Page<LibraryItem>>> GetItems(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 50)
{
try
{
ICollection<LibraryItem> resources = await _libraryManager.GetItemsFromLibrary(slug,
ApiHelper.ParseWhere<LibraryItem>(where),
new Sort<LibraryItem>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Library>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
} }
} }
} }