Swagger: sorting tags groups & documenting the genre's API

This commit is contained in:
Zoe Roux 2021-09-26 18:20:46 +02:00
parent 049f545d51
commit 0fdc583d58
7 changed files with 301 additions and 165 deletions

View File

@ -33,7 +33,9 @@ namespace Kyoo.Abstractions.Models.Attributes
[NotNull] public string Name { get; }
/// <summary>
/// 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: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
/// th alphabetical ordering.
/// </summary>
public string Group { get; set; }

View File

@ -34,11 +34,17 @@ namespace Kyoo.Abstractions.Models.Utils
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
/// </summary>
public const string ResourcesGroup = "Resources";
public const string ResourcesGroup = "0:Resources";
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>.
/// It should be used for sub resources of kyoo that help define the main resources.
/// </summary>
public const string MetadataGroup = "1:Metadata";
/// <summary>
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
/// </summary>
public const string WatchGroup = "Watch";
public const string WatchGroup = "2:Watch";
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Genre>
{
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<ActionResult<Page<Show>>> GetShows(int id,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.ID == id)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(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<ActionResult<Page<Show>>> GetShows(string slug,
[FromQuery] string sortBy,
[FromQuery] int afterID,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere<Show>(where, x => x.Genres.Any(y => y.Slug == slug)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID));
if (!resources.Any() && await _libraryManager.GetOrDefault<Genre>(slug) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new { Error = ex.Message });
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// Information about one or multiple <see cref="Genre"/>.
/// </summary>
[Route("api/genres")]
[Route("api/genre", Order = AlternativeRoute)]
[ApiController]
[PartialPermission(nameof(GenreApi))]
[ApiDefinition("Genres", Group = MetadataGroup)]
public class GenreApi : CrudApi<Genre>
{
/// <summary>
/// The library manager used to modify or retrieve information about the data store.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Create a new <see cref="GenreApi"/>.
/// </summary>
/// <param name="libraryManager">
/// The library manager used to modify or retrieve information about the data store.
/// </param>
public GenreApi(ILibraryManager libraryManager)
: base(libraryManager.GenreRepository)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Get shows with genre
/// </summary>
/// <remarks>
/// Lists the shows that have the selected genre.
/// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Genre"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param>
/// <param name="limit">The number of shows to return.</param>
/// <param name="afterID">An optional show's ID to start the query from this specific item.</param>
/// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No genre with the given ID could be found.</response>
[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<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] string sortBy,
[FromQuery] Dictionary<string, string> where,
[FromQuery] int limit = 20,
[FromQuery] int? afterID = null)
{
try
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Genre>(x => x.Genres)),
new Sort<Show>(sortBy),
new Pagination(limit, afterID)
);
if (!resources.Any() && await _libraryManager.GetOrDefault(identifier.IsSame<Genre>()) == null)
return NotFound();
return Page(resources, limit);
}
catch (ArgumentException ex)
{
return BadRequest(new RequestError(ex.Message));
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Linq;
using NSwag;
using NSwag.Generation.AspNetCore;
namespace Kyoo.Swagger
{
/// <summary>
/// A class to sort apis.
/// </summary>
public static class ApiSorter
{
/// <summary>
/// Sort apis by alphabetical orders.
/// </summary>
/// <param name="options">The swagger settings to update.</param>
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<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);
};
options.PostProcess += postProcess =>
{
if (!postProcess.ExtensionData.TryGetValue("x-tagGroups", out object list))
return;
List<dynamic> tagGroups = (List<dynamic>)list;
postProcess.ExtensionData["x-tagGroups"] = tagGroups
.OrderBy(x => x.name)
.Select(x => new
{
name = x.name.Substring(x.name.IndexOf(':') + 1),
x.tags
})
.ToList();
};
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
{
/// <summary>
/// A class to handle Api Groups (OpenApi tags and x-tagGroups).
/// Tags should be specified via <see cref="ApiDefinitionAttribute"/> and this filter will map this to the
/// <see cref="OpenApiDocument"/>.
/// </summary>
public static class ApiTagsFilter
{
/// <summary>
/// The main operation filter that will map every <see cref="ApiDefinitionAttribute"/>.
/// </summary>
/// <param name="context">The processor context, this is given by the AddOperationFilter method.</param>
/// <returns>This always return <c>true</c> since it should not remove operations.</returns>
public static bool OperationFilter(OperationProcessorContext 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="postProcess">
/// The document to do this for. This should be done in the PostProcess part of the document or after
/// the main operation filter (see <see cref="OperationFilter"/>) has finished.
/// </param>
public static void AddLeftoversToOthersGroup(this OpenApiDocument postProcess)
{
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
});
}
}
/// <summary>
/// Use <see cref="ApiDefinitionAttribute"/> to create tags and groups of tags on the resulting swagger
/// document.
/// </summary>
/// <param name="options">The settings of the swagger document.</param>
public static void UseApiTags(this AspNetCoreOpenApiDocumentGeneratorSettings options)
{
options.AddOperationFilter(OperationFilter);
options.PostProcess += x => x.AddLeftoversToOthersGroup();
}
}
}

View File

@ -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<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.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<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;