Swagger: handling tags and sort order

This commit is contained in:
Zoe Roux 2021-09-22 14:44:21 +02:00
parent 9b3eb7fede
commit 2a22661c46
5 changed files with 176 additions and 23 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Attributes
{
/// <summary>
/// An attribute to specify on apis to specify it's documentation's name and category.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ApiDefinitionAttribute : Attribute
{
/// <summary>
/// The public name of this api.
/// </summary>
[NotNull] public string Name { get; }
/// <summary>
/// The name of the group in witch this API is.
/// </summary>
public string Group { get; set; }
/// <summary>
/// Create a new <see cref="ApiDefinitionAttribute"/>.
/// </summary>
/// <param name="name">The name of the api that will be used on the documentation page.</param>
public ApiDefinitionAttribute([NotNull] string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
Name = name;
}
}
}

View File

@ -16,6 +16,8 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils
{
/// <summary>
@ -28,5 +30,10 @@ namespace Kyoo.Abstractions.Models.Utils
/// that won't be included on the swagger.
/// </summary>
public const int AlternativeRoute = 1;
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for every <see cref="IResource"/>.
/// </summary>
public const string ResourceGroup = "Resource";
}
}

View File

@ -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<Collection>
{
/// <summary>
@ -46,6 +49,17 @@ namespace Kyoo.Core.Api
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="CollectionApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
/// <param name="options">
/// Options used to retrieve the base URL of Kyoo.
/// </param>
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<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,

View File

@ -28,12 +28,37 @@ using static Kyoo.Abstractions.Models.Utils.Constants;
namespace Kyoo.Core.Api
{
/// <summary>
/// A base class to handle CRUD operations and services thumbnails for
/// a specific resource type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of resource to make CRUD and thumbnails apis for.</typeparam>
[ApiController]
[ResourceView]
public class CrudThumbsApi<T> : CrudApi<T>
where T : class, IResource, IThumbnails
{
/// <summary>
/// The file manager used to send images.
/// </summary>
private readonly IFileSystem _files;
/// <summary>
/// The thumbnail manager used to retrieve images paths.
/// </summary>
private readonly IThumbnailsManager _thumbs;
/// <summary>
/// Create a new <see cref="CrudThumbsApi{T}"/> that handles crud requests and thumbnails.
/// </summary>
/// <param name="repository">
/// The repository to use as a baking store for the type <typeparamref name="T"/>.
/// </param>
/// <param name="files">The file manager used to send images.</param>
/// <param name="thumbs">The thumbnail manager used to retrieve images paths.</param>
/// <param name="baseURL">
/// The base URL of Kyoo to use to create links.
/// </param>
public CrudThumbsApi(IRepository<T> repository,
IFileSystem files,
IThumbnailsManager thumbs,
@ -49,26 +74,17 @@ namespace Kyoo.Core.Api
/// </summary>
/// <remarks>
/// Get an image for the specified item.
/// List of commonly available images:
/// <list type="bullet">
/// <item>
/// <description>Poster: Image 0, also available at /poster</description>
/// </item>
/// <item>
/// <description>Thumbnail: Image 1, also available at /thumbnail</description>
/// </item>
/// <item>
/// <description>Logo: Image 3, also available at /logo</description>
/// </item>
/// </list>
/// List of commonly available images:<br/>
/// - Poster: Image 0, also available at /poster<br/>
/// - Thumbnail: Image 1, also available at /thumbnail<br/>
/// - Logo: Image 3, also available at /logo<br/>
/// <br/>
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
/// </remarks>
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
/// <param name="image">The number of the image to retrieve.</param>
/// <returns>The image asked.</returns>
/// <response code="404">
/// No item exist with the specific identifier or the image does not exists on kyoo.
/// </response>
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
[HttpGet("{identifier:id}/image-{image:int}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]

View File

@ -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<KeyValuePair<string, OpenApiPathItem>> sorted = postProcess.Paths
.OrderBy(x => x.Key)
.ToList();
postProcess.Paths.Clear();
foreach ((string key, OpenApiPathItem value) in sorted)
postProcess.Paths.Add(key, value);
List<dynamic> tagGroups = (List<dynamic>)postProcess.ExtensionData["x-tagGroups"];
List<string> tagsWithoutGroup = postProcess.Tags
.Select(x => x.Name)
.Where(x => tagGroups
.SelectMany<dynamic, string>(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<ApiDefinitionAttribute>();
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<string, object>();
context.Document.ExtensionData.TryAdd("x-tagGroups", new List<dynamic>());
List<dynamic> obj = (List<dynamic>)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<string> { 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<IStartupAction> ConfigureSteps => new IStartupAction[]
{
SA.New<IApplicationBuilder>(app => app.UseOpenApi(), SA.Before + 1),
SA.New<IApplicationBuilder>(app => app.UseSwaggerUi3(x =>
{
x.OperationsSorter = "alpha";
x.TagsSorter = "alpha";
}), SA.Before),
SA.New<IApplicationBuilder>(app => app.UseSwaggerUi3(), SA.Before),
SA.New<IApplicationBuilder>(app => app.UseReDoc(x =>
{
x.Path = "/redoc";