mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Swagger: handling tags and sort order
This commit is contained in:
parent
9b3eb7fede
commit
2a22661c46
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
namespace Kyoo.Abstractions.Models.Utils
|
namespace Kyoo.Abstractions.Models.Utils
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -28,5 +30,10 @@ namespace Kyoo.Abstractions.Models.Utils
|
|||||||
/// that won't be included on the swagger.
|
/// that won't be included on the swagger.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int AlternativeRoute = 1;
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,12 +22,14 @@ using System.Linq;
|
|||||||
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.Abstractions.Models.Utils;
|
||||||
using Kyoo.Core.Models.Options;
|
using Kyoo.Core.Models.Options;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using NSwag.Annotations;
|
||||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||||
|
|
||||||
namespace Kyoo.Core.Api
|
namespace Kyoo.Core.Api
|
||||||
@ -39,6 +41,7 @@ namespace Kyoo.Core.Api
|
|||||||
[Route("api/collection", Order = AlternativeRoute)]
|
[Route("api/collection", Order = AlternativeRoute)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[PartialPermission(nameof(CollectionApi))]
|
[PartialPermission(nameof(CollectionApi))]
|
||||||
|
[ApiDefinition("Collection", Group = ResourceGroup)]
|
||||||
public class CollectionApi : CrudThumbsApi<Collection>
|
public class CollectionApi : CrudThumbsApi<Collection>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -46,6 +49,17 @@ namespace Kyoo.Core.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILibraryManager _libraryManager;
|
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,
|
public CollectionApi(ILibraryManager libraryManager,
|
||||||
IFileSystem files,
|
IFileSystem files,
|
||||||
IThumbnailsManager thumbs,
|
IThumbnailsManager thumbs,
|
||||||
@ -115,6 +129,9 @@ namespace Kyoo.Core.Api
|
|||||||
[HttpGet("{identifier:id}/libraries")]
|
[HttpGet("{identifier:id}/libraries")]
|
||||||
[HttpGet("{identifier:id}/library", Order = AlternativeRoute)]
|
[HttpGet("{identifier:id}/library", Order = AlternativeRoute)]
|
||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
|
public async Task<ActionResult<Page<Library>>> GetLibraries(Identifier identifier,
|
||||||
[FromQuery] string sortBy,
|
[FromQuery] string sortBy,
|
||||||
[FromQuery] Dictionary<string, string> where,
|
[FromQuery] Dictionary<string, string> where,
|
||||||
|
@ -28,12 +28,37 @@ using static Kyoo.Abstractions.Models.Utils.Constants;
|
|||||||
|
|
||||||
namespace Kyoo.Core.Api
|
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>
|
public class CrudThumbsApi<T> : CrudApi<T>
|
||||||
where T : class, IResource, IThumbnails
|
where T : class, IResource, IThumbnails
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The file manager used to send images.
|
||||||
|
/// </summary>
|
||||||
private readonly IFileSystem _files;
|
private readonly IFileSystem _files;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The thumbnail manager used to retrieve images paths.
|
||||||
|
/// </summary>
|
||||||
private readonly IThumbnailsManager _thumbs;
|
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,
|
public CrudThumbsApi(IRepository<T> repository,
|
||||||
IFileSystem files,
|
IFileSystem files,
|
||||||
IThumbnailsManager thumbs,
|
IThumbnailsManager thumbs,
|
||||||
@ -49,26 +74,17 @@ namespace Kyoo.Core.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Get an image for the specified item.
|
/// Get an image for the specified item.
|
||||||
/// List of commonly available images:
|
/// List of commonly available images:<br/>
|
||||||
/// <list type="bullet">
|
/// - Poster: Image 0, also available at /poster<br/>
|
||||||
/// <item>
|
/// - Thumbnail: Image 1, also available at /thumbnail<br/>
|
||||||
/// <description>Poster: Image 0, also available at /poster</description>
|
/// - Logo: Image 3, also available at /logo<br/>
|
||||||
/// </item>
|
/// <br/>
|
||||||
/// <item>
|
|
||||||
/// <description>Thumbnail: Image 1, also available at /thumbnail</description>
|
|
||||||
/// </item>
|
|
||||||
/// <item>
|
|
||||||
/// <description>Logo: Image 3, also available at /logo</description>
|
|
||||||
/// </item>
|
|
||||||
/// </list>
|
|
||||||
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
|
/// Other images can be arbitrarily added by plugins so any image number can be specified from this endpoint.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="identifier">The ID or slug of the resource to get the image for.</param>
|
/// <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>
|
/// <param name="image">The number of the image to retrieve.</param>
|
||||||
/// <returns>The image asked.</returns>
|
/// <returns>The image asked.</returns>
|
||||||
/// <response code="404">
|
/// <response code="404">No item exist with the specific identifier or the image does not exists on kyoo.</response>
|
||||||
/// No item exist with the specific identifier or the image does not exists on kyoo.
|
|
||||||
/// </response>
|
|
||||||
[HttpGet("{identifier:id}/image-{image:int}")]
|
[HttpGet("{identifier:id}/image-{image:int}")]
|
||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
@ -18,11 +18,15 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
using Kyoo.Abstractions.Models.Utils;
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Namotion.Reflection;
|
||||||
using NJsonSchema;
|
using NJsonSchema;
|
||||||
using NJsonSchema.Generation.TypeMappers;
|
using NJsonSchema.Generation.TypeMappers;
|
||||||
using NSwag;
|
using NSwag;
|
||||||
@ -61,18 +65,42 @@ namespace Kyoo.Swagger
|
|||||||
options.DocumentName = "v1";
|
options.DocumentName = "v1";
|
||||||
options.UseControllerSummaryAsTagDescription = true;
|
options.UseControllerSummaryAsTagDescription = true;
|
||||||
options.GenerateExamples = true;
|
options.GenerateExamples = true;
|
||||||
options.PostProcess = x =>
|
options.PostProcess = postProcess =>
|
||||||
{
|
{
|
||||||
x.Info.Contact = new OpenApiContact
|
postProcess.Info.Contact = new OpenApiContact
|
||||||
{
|
{
|
||||||
Name = "Kyoo's github",
|
Name = "Kyoo's github",
|
||||||
Url = "https://github.com/AnonymusRaccoon/Kyoo"
|
Url = "https://github.com/AnonymusRaccoon/Kyoo"
|
||||||
};
|
};
|
||||||
x.Info.License = new OpenApiLicense
|
postProcess.Info.License = new OpenApiLicense
|
||||||
{
|
{
|
||||||
Name = "GPL-3.0-or-later",
|
Name = "GPL-3.0-or-later",
|
||||||
Url = "https://github.com/AnonymusRaccoon/Kyoo/blob/master/LICENSE"
|
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 =>
|
options.AddOperationFilter(x =>
|
||||||
{
|
{
|
||||||
@ -80,6 +108,44 @@ namespace Kyoo.Swagger
|
|||||||
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
|
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
|
||||||
return true;
|
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 =>
|
options.SchemaGenerator.Settings.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Identifier), x =>
|
||||||
{
|
{
|
||||||
x.IsNullableRaw = false;
|
x.IsNullableRaw = false;
|
||||||
@ -92,11 +158,7 @@ namespace Kyoo.Swagger
|
|||||||
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
|
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
|
||||||
{
|
{
|
||||||
SA.New<IApplicationBuilder>(app => app.UseOpenApi(), SA.Before + 1),
|
SA.New<IApplicationBuilder>(app => app.UseOpenApi(), SA.Before + 1),
|
||||||
SA.New<IApplicationBuilder>(app => app.UseSwaggerUi3(x =>
|
SA.New<IApplicationBuilder>(app => app.UseSwaggerUi3(), SA.Before),
|
||||||
{
|
|
||||||
x.OperationsSorter = "alpha";
|
|
||||||
x.TagsSorter = "alpha";
|
|
||||||
}), SA.Before),
|
|
||||||
SA.New<IApplicationBuilder>(app => app.UseReDoc(x =>
|
SA.New<IApplicationBuilder>(app => app.UseReDoc(x =>
|
||||||
{
|
{
|
||||||
x.Path = "/redoc";
|
x.Path = "/redoc";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user