From 2a22661c4689dc131c663485093ede90ea4991d0 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 22 Sep 2021 14:44:21 +0200 Subject: [PATCH] Swagger: handling tags and sort order --- .../Attributes/ApiDefinitionAttribute.cs | 51 ++++++++++++ .../Models/Utils/Constants.cs | 7 ++ src/Kyoo.Core/Views/CollectionApi.cs | 17 ++++ src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs | 46 +++++++---- src/Kyoo.Swagger/SwaggerModule.cs | 78 +++++++++++++++++-- 5 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs new file mode 100644 index 00000000..84d8dc3d --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/ApiDefinitionAttribute.cs @@ -0,0 +1,51 @@ +// 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 JetBrains.Annotations; + +namespace Kyoo.Abstractions.Models.Attributes +{ + /// + /// An attribute to specify on apis to specify it's documentation's name and category. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ApiDefinitionAttribute : Attribute + { + /// + /// The public name of this api. + /// + [NotNull] public string Name { get; } + + /// + /// The name of the group in witch this API is. + /// + public string Group { get; set; } + + /// + /// Create a new . + /// + /// The name of the api that will be used on the documentation page. + public ApiDefinitionAttribute([NotNull] string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + Name = name; + } + } +} diff --git a/src/Kyoo.Abstractions/Models/Utils/Constants.cs b/src/Kyoo.Abstractions/Models/Utils/Constants.cs index 595877fb..190307af 100644 --- a/src/Kyoo.Abstractions/Models/Utils/Constants.cs +++ b/src/Kyoo.Abstractions/Models/Utils/Constants.cs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU General Public License // along with Kyoo. If not, see . +using Kyoo.Abstractions.Models.Attributes; + namespace Kyoo.Abstractions.Models.Utils { /// @@ -28,5 +30,10 @@ namespace Kyoo.Abstractions.Models.Utils /// that won't be included on the swagger. /// public const int AlternativeRoute = 1; + + /// + /// A group name for . It should be used for every . + /// + public const string ResourceGroup = "Resource"; } } diff --git a/src/Kyoo.Core/Views/CollectionApi.cs b/src/Kyoo.Core/Views/CollectionApi.cs index b9a627cf..22b0608e 100644 --- a/src/Kyoo.Core/Views/CollectionApi.cs +++ b/src/Kyoo.Core/Views/CollectionApi.cs @@ -22,12 +22,14 @@ 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 Kyoo.Core.Models.Options; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using NSwag.Annotations; using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api @@ -39,6 +41,7 @@ namespace Kyoo.Core.Api [Route("api/collection", Order = AlternativeRoute)] [ApiController] [PartialPermission(nameof(CollectionApi))] + [ApiDefinition("Collection", Group = ResourceGroup)] public class CollectionApi : CrudThumbsApi { /// @@ -46,6 +49,17 @@ namespace Kyoo.Core.Api /// private readonly ILibraryManager _libraryManager; + /// + /// Create a new . + /// + /// + /// The library manager used to modify or retrieve information about the data store. + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// Options used to retrieve the base URL of Kyoo. + /// public CollectionApi(ILibraryManager libraryManager, IFileSystem files, IThumbnailsManager thumbs, @@ -115,6 +129,9 @@ namespace Kyoo.Core.Api [HttpGet("{identifier:id}/libraries")] [HttpGet("{identifier:id}/library", Order = AlternativeRoute)] [PartialPermission(Kind.Read)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetLibraries(Identifier identifier, [FromQuery] string sortBy, [FromQuery] Dictionary where, diff --git a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs index 7d3ccc42..95a5782c 100644 --- a/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudThumbsApi.cs @@ -28,12 +28,37 @@ using static Kyoo.Abstractions.Models.Utils.Constants; namespace Kyoo.Core.Api { + /// + /// A base class to handle CRUD operations and services thumbnails for + /// a specific resource type . + /// + /// The type of resource to make CRUD and thumbnails apis for. + [ApiController] + [ResourceView] public class CrudThumbsApi : CrudApi where T : class, IResource, IThumbnails { + /// + /// The file manager used to send images. + /// private readonly IFileSystem _files; + + /// + /// The thumbnail manager used to retrieve images paths. + /// private readonly IThumbnailsManager _thumbs; + /// + /// Create a new that handles crud requests and thumbnails. + /// + /// + /// The repository to use as a baking store for the type . + /// + /// The file manager used to send images. + /// The thumbnail manager used to retrieve images paths. + /// + /// The base URL of Kyoo to use to create links. + /// public CrudThumbsApi(IRepository repository, IFileSystem files, IThumbnailsManager thumbs, @@ -49,26 +74,17 @@ namespace Kyoo.Core.Api /// /// /// Get an image for the specified item. - /// List of commonly available images: - /// - /// - /// Poster: Image 0, also available at /poster - /// - /// - /// Thumbnail: Image 1, also available at /thumbnail - /// - /// - /// Logo: Image 3, also available at /logo - /// - /// + /// List of commonly available images:
+ /// - Poster: Image 0, also available at /poster
+ /// - Thumbnail: Image 1, also available at /thumbnail
+ /// - Logo: Image 3, also available at /logo
+ ///
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint. ///
/// The ID or slug of the resource to get the image for. /// The number of the image to retrieve. /// The image asked. - /// - /// No item exist with the specific identifier or the image does not exists on kyoo. - /// + /// No item exist with the specific identifier or the image does not exists on kyoo. [HttpGet("{identifier:id}/image-{image:int}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 4d743b3a..ad98de7d 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -18,11 +18,15 @@ 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; @@ -61,18 +65,42 @@ namespace Kyoo.Swagger options.DocumentName = "v1"; options.UseControllerSummaryAsTagDescription = true; options.GenerateExamples = true; - options.PostProcess = x => + options.PostProcess = postProcess => { - x.Info.Contact = new OpenApiContact + postProcess.Info.Contact = new OpenApiContact { Name = "Kyoo's github", Url = "https://github.com/AnonymusRaccoon/Kyoo" }; - x.Info.License = new OpenApiLicense + postProcess.Info.License = new OpenApiLicense { 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.AddOperationFilter(x => { @@ -80,6 +108,44 @@ namespace Kyoo.Swagger 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; @@ -92,11 +158,7 @@ namespace Kyoo.Swagger public IEnumerable ConfigureSteps => new IStartupAction[] { SA.New(app => app.UseOpenApi(), SA.Before + 1), - SA.New(app => app.UseSwaggerUi3(x => - { - x.OperationsSorter = "alpha"; - x.TagsSorter = "alpha"; - }), SA.Before), + SA.New(app => app.UseSwaggerUi3(), SA.Before), SA.New(app => app.UseReDoc(x => { x.Path = "/redoc";