From 0fdc583d582b489a2249d9491e241fdb92f64de6 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 26 Sep 2021 18:20:46 +0200 Subject: [PATCH] Swagger: sorting tags groups & documenting the genre's API --- .../Attributes/ApiDefinitionAttribute.cs | 4 +- .../Models/Utils/Constants.cs | 10 +- src/Kyoo.Core/Views/GenreApi.cs | 96 -------------- src/Kyoo.Core/Views/Metadata/GenreApi.cs | 105 ++++++++++++++++ src/Kyoo.Swagger/ApiSorter.cs | 64 ++++++++++ src/Kyoo.Swagger/ApiTagsFilter.cs | 119 ++++++++++++++++++ src/Kyoo.Swagger/SwaggerModule.cs | 68 +--------- 7 files changed, 301 insertions(+), 165 deletions(-) delete mode 100644 src/Kyoo.Core/Views/GenreApi.cs create mode 100644 src/Kyoo.Core/Views/Metadata/GenreApi.cs create mode 100644 src/Kyoo.Swagger/ApiSorter.cs create mode 100644 src/Kyoo.Swagger/ApiTagsFilter.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs index 84d8dc3d..b8923eef 100644 --- a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs +++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -33,7 +33,9 @@ namespace Kyoo.Abstractions.Models.Attributes [NotNull] public string Name { get; } /// - /// The name of the group in witch this API is. + /// The name of the group in witch this API is. You can also specify a custom sort order using the following + /// format: order:name. Everything before the first : will be removed but kept for + /// th alphabetical ordering. /// public string Group { get; set; } diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 521dc2d2..985f0254 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -34,11 +34,17 @@ namespace Kyoo.Abstractions.Models.Utils /// /// A group name for . It should be used for main resources of kyoo. /// - public const string ResourcesGroup = "Resources"; + public const string ResourcesGroup = "0:Resources"; + + /// + /// A group name for . + /// It should be used for sub resources of kyoo that help define the main resources. + /// + public const string MetadataGroup = "1:Metadata"; /// /// A group name for . It should be used for endpoints useful for playback. /// - public const string WatchGroup = "Watch"; + public const string WatchGroup = "2:Watch"; } } diff --git a/src/Kyoo.Core/Views/GenreApi.cs b/src/Kyoo.Core/Views/GenreApi.cs deleted file mode 100644 index 28bc3067..00000000 --- a/src/Kyoo.Core/Views/GenreApi.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Kyoo - A portable and vast media library solution. -// Copyright (c) Kyoo. -// -// See AUTHORS.md and LICENSE file in the project root for full license information. -// -// Kyoo is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// Kyoo is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kyoo. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models; -using Kyoo.Abstractions.Models.Permissions; -using Microsoft.AspNetCore.Mvc; - -namespace Kyoo.Core.Api -{ - [Route("api/genre")] - [Route("api/genres")] - [ApiController] - [PartialPermission(nameof(GenreApi))] - public class GenreApi : CrudApi - { - private readonly ILibraryManager _libraryManager; - - public GenreApi(ILibraryManager libraryManager) - : base(libraryManager.GenreRepository) - { - _libraryManager = libraryManager; - } - - [HttpGet("{id:int}/show")] - [HttpGet("{id:int}/shows")] - [PartialPermission(Kind.Read)] - public async Task>> GetShows(int id, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.ID == id)), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(id) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - - [HttpGet("{slug}/show")] - [HttpGet("{slug}/shows")] - [PartialPermission(Kind.Read)] - public async Task>> GetShows(string slug, - [FromQuery] string sortBy, - [FromQuery] int afterID, - [FromQuery] Dictionary where, - [FromQuery] int limit = 20) - { - try - { - ICollection resources = await _libraryManager.GetAll( - ApiHelper.ParseWhere(where, x => x.Genres.Any(y => y.Slug == slug)), - new Sort(sortBy), - new Pagination(limit, afterID)); - - if (!resources.Any() && await _libraryManager.GetOrDefault(slug) == null) - return NotFound(); - return Page(resources, limit); - } - catch (ArgumentException ex) - { - return BadRequest(new { Error = ex.Message }); - } - } - } -} diff --git a/src/Kyoo.Core/Views/Metadata/GenreApi.cs b/src/Kyoo.Core/Views/Metadata/GenreApi.cs new file mode 100644 index 00000000..57336138 --- /dev/null +++ b/src/Kyoo.Core/Views/Metadata/GenreApi.cs @@ -0,0 +1,105 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; +using Kyoo.Abstractions.Models.Permissions; +using Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using static Kyoo.Abstractions.Models.Utils.Constants; + +namespace Kyoo.Core.Api +{ + /// + /// Information about one or multiple . + /// + [Route("api/genres")] + [Route("api/genre", Order = AlternativeRoute)] + [ApiController] + [PartialPermission(nameof(GenreApi))] + [ApiDefinition("Genres", Group = MetadataGroup)] + public class GenreApi : CrudApi + { + /// + /// The library manager used to modify or retrieve information about the data store. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information about the data store. + /// + public GenreApi(ILibraryManager libraryManager) + : base(libraryManager.GenreRepository) + { + _libraryManager = libraryManager; + } + + /// + /// Get shows with genre + /// + /// + /// Lists the shows that have the selected genre. + /// + /// The ID or slug of the . + /// A key to sort shows by. + /// An optional list of filters. + /// The number of shows to return. + /// An optional show's ID to start the query from this specific item. + /// A page of shows. + /// The filters or the sort parameters are invalid. + /// No genre with the given ID could be found. + [HttpGet("{identifier:id}/shows")] + [HttpGet("{identifier:id}/show", Order = AlternativeRoute)] + [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetShows(Identifier identifier, + [FromQuery] string sortBy, + [FromQuery] Dictionary where, + [FromQuery] int limit = 20, + [FromQuery] int? afterID = null) + { + try + { + ICollection resources = await _libraryManager.GetAll( + ApiHelper.ParseWhere(where, identifier.IsContainedIn(x => x.Genres)), + new Sort(sortBy), + new Pagination(limit, afterID) + ); + + if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame()) == null) + return NotFound(); + return Page(resources, limit); + } + catch (ArgumentException ex) + { + return BadRequest(new RequestError(ex.Message)); + } + } + } +} diff --git a/src/Kyoo.Swagger/ApiSorter.cs b/src/Kyoo.Swagger/ApiSorter.cs new file mode 100644 index 00000000..f328d354 --- /dev/null +++ b/src/Kyoo.Swagger/ApiSorter.cs @@ -0,0 +1,64 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Collections.Generic; +using System.Linq; +using NSwag; +using NSwag.Generation.AspNetCore; + +namespace Kyoo.Swagger +{ + /// + /// A class to sort apis. + /// + public static class ApiSorter + { + /// + /// Sort apis by alphabetical orders. + /// + /// The swagger settings to update. + public static void SortApis(this AspNetCoreOpenApiDocumentGeneratorSettings options) + { + options.PostProcess += postProcess => + { + // We can't reorder items by assigning the sorted value to the Paths variable since it has no setter. + List> sorted = postProcess.Paths + .OrderBy(x => x.Key) + .ToList(); + postProcess.Paths.Clear(); + foreach ((string key, OpenApiPathItem value) in sorted) + postProcess.Paths.Add(key, value); + }; + + options.PostProcess += postProcess => + { + if (!postProcess.ExtensionData.TryGetValue("x-tagGroups", out object list)) + return; + List tagGroups = (List)list; + postProcess.ExtensionData["x-tagGroups"] = tagGroups + .OrderBy(x => x.name) + .Select(x => new + { + name = x.name.Substring(x.name.IndexOf(':') + 1), + x.tags + }) + .ToList(); + }; + } + } +} diff --git a/src/Kyoo.Swagger/ApiTagsFilter.cs b/src/Kyoo.Swagger/ApiTagsFilter.cs new file mode 100644 index 00000000..9ee21a01 --- /dev/null +++ b/src/Kyoo.Swagger/ApiTagsFilter.cs @@ -0,0 +1,119 @@ +// Kyoo - A portable and vast media library solution. +// Copyright (c) Kyoo. +// +// See AUTHORS.md and LICENSE file in the project root for full license information. +// +// Kyoo is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// Kyoo is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kyoo. If not, see . + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Kyoo.Abstractions.Models.Attributes; +using Namotion.Reflection; +using NSwag; +using NSwag.Generation.AspNetCore; +using NSwag.Generation.Processors.Contexts; + +namespace Kyoo.Swagger +{ + /// + /// A class to handle Api Groups (OpenApi tags and x-tagGroups). + /// Tags should be specified via and this filter will map this to the + /// . + /// + public static class ApiTagsFilter + { + /// + /// The main operation filter that will map every . + /// + /// The processor context, this is given by the AddOperationFilter method. + /// This always return true since it should not remove operations. + public static bool OperationFilter(OperationProcessorContext context) + { + ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute(); + string name = def?.Name ?? context.ControllerType.Name; + + context.OperationDescription.Operation.Tags.Add(name); + if (context.Document.Tags.All(x => x.Name != name)) + { + context.Document.Tags.Add(new OpenApiTag + { + Name = name, + Description = context.ControllerType.GetXmlDocsSummary() + }); + } + + if (def == null) + return true; + + context.Document.ExtensionData ??= new Dictionary(); + context.Document.ExtensionData.TryAdd("x-tagGroups", new List()); + List obj = (List)context.Document.ExtensionData["x-tagGroups"]; + dynamic existing = obj.FirstOrDefault(x => x.name == def.Group); + if (existing != null) + { + if (!existing.tags.Contains(def.Name)) + existing.tags.Add(def.Name); + } + else + { + obj.Add(new + { + name = def.Group, + tags = new List { def.Name } + }); + } + + return true; + } + + /// + /// This add every tags that are not in a x-tagGroups to a new tagGroups named "Other". + /// Since tags that are not in a tagGroups are not shown, this is necessary if you want them displayed. + /// + /// + /// The document to do this for. This should be done in the PostProcess part of the document or after + /// the main operation filter (see ) has finished. + /// + public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess) + { + List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"]; + List tagsWithoutGroup = postProcess.Tags + .Select(x => x.Name) + .Where(x => tagGroups + .SelectMany(y => y.tags) + .All(y => y != x)) + .ToList(); + if (tagsWithoutGroup.Any()) + { + tagGroups.Add(new + { + name = "Others", + tags = tagsWithoutGroup + }); + } + } + + /// + /// Use to create tags and groups of tags on the resulting swagger + /// document. + /// + /// The settings of the swagger document. + public static void UseApiTags(this AspNetCoreOpenApiDocumentGeneratorSettings options) + { + options.AddOperationFilter(OperationFilter); + options.PostProcess += x => x.AddLeftoversToOthersGroup(); + } + } +} diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index ad98de7d..4cbff4a1 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -18,15 +18,11 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; using Kyoo.Abstractions.Controllers; -using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.DependencyInjection; -using Namotion.Reflection; using NJsonSchema; using NJsonSchema.Generation.TypeMappers; using NSwag; @@ -77,75 +73,15 @@ namespace Kyoo.Swagger Name = "GPL-3.0-or-later", Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE" }; - - // We can't reorder items by assigning the sorted value to the Paths variable since it has no setter. - List> sorted = postProcess.Paths - .OrderBy(x => x.Key) - .ToList(); - postProcess.Paths.Clear(); - foreach ((string key, OpenApiPathItem value) in sorted) - postProcess.Paths.Add(key, value); - - List tagGroups = (List)postProcess.ExtensionData["x-tagGroups"]; - List tagsWithoutGroup = postProcess.Tags - .Select(x => x.Name) - .Where(x => tagGroups - .SelectMany(y => y.tags) - .All(y => y != x)) - .ToList(); - if (tagsWithoutGroup.Any()) - { - tagGroups.Add(new - { - name = "Others", - tags = tagsWithoutGroup - }); - } }; + options.UseApiTags(); + options.SortApis(); options.AddOperationFilter(x => { if (x is AspNetCoreOperationProcessorContext ctx) return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; return true; }); - options.AddOperationFilter(context => - { - ApiDefinitionAttribute def = context.ControllerType.GetCustomAttribute(); - string name = def?.Name ?? context.ControllerType.Name; - - context.OperationDescription.Operation.Tags.Add(name); - if (context.Document.Tags.All(x => x.Name != name)) - { - context.Document.Tags.Add(new OpenApiTag - { - Name = name, - Description = context.ControllerType.GetXmlDocsSummary() - }); - } - - if (def == null) - return true; - - context.Document.ExtensionData ??= new Dictionary(); - context.Document.ExtensionData.TryAdd("x-tagGroups", new List()); - List obj = (List)context.Document.ExtensionData["x-tagGroups"]; - dynamic existing = obj.FirstOrDefault(x => x.name == def.Group); - if (existing != null) - { - if (!existing.tags.Contains(def.Name)) - existing.tags.Add(def.Name); - } - else - { - obj.Add(new - { - name = def.Group, - tags = new List { def.Name } - }); - } - - return true; - }); options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x => { x.IsNullableRaw = false;