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";