From 41cbc50940f4dbc5b495c9ae4565c6a1d869387b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 21 Sep 2021 16:42:49 +0200 Subject: [PATCH] API: Starting to merge id/slug routes using a new Identifier --- .../Models/Utils/Identifier.cs | 112 ++++++++++++++++++ .../Controllers/IdentifierRouteConstraint.cs | 40 +++++++ src/Kyoo.Core/CoreModule.cs | 17 ++- src/Kyoo.Core/Views/Helper/CrudApi.cs | 82 ++++--------- src/Kyoo.Swagger/SwaggerModule.cs | 8 ++ 5 files changed, 191 insertions(+), 68 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Utils/Identifier.cs create mode 100644 src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs diff --git a/src/Kyoo.Abstractions/Models/Utils/Identifier.cs b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs new file mode 100644 index 00000000..356ff3d8 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Utils/Identifier.cs @@ -0,0 +1,112 @@ +// 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 System.ComponentModel; +using System.Globalization; +using JetBrains.Annotations; + +namespace Kyoo.Abstractions.Models.Utils +{ + /// + /// A class that represent a resource. It is made to be used as a parameter in a query and not used somewhere else + /// on the application. + /// This class allow routes to be used via ether IDs or Slugs, this is suitable for every . + /// + [TypeConverter(typeof(IdentifierConvertor))] + public class Identifier + { + /// + /// The ID of the resource or null if the slug is specified. + /// + private readonly int? _id; + + /// + /// The slug of the resource or null if the id is specified. + /// + private readonly string _slug; + + /// + /// Create a new for the given id. + /// + /// The id of the resource. + public Identifier(int id) + { + _id = id; + } + + /// + /// Create a new for the given slug. + /// + /// The slug of the resource. + public Identifier([NotNull] string slug) + { + if (slug == null) + throw new ArgumentNullException(nameof(slug)); + _slug = slug; + } + + /// + /// Pattern match out of the identifier to a resource. + /// + /// The function to match the ID to a type . + /// The function to match the slug to a type . + /// The return type that will be converted to from an ID or a slug. + /// + /// The result of the or depending on the pattern. + /// + /// + /// Example usage: + /// + /// T ret = await identifier.Match( + /// id => _repository.GetOrDefault(id), + /// slug => _repository.GetOrDefault(slug) + /// ); + /// + /// + public T Match(Func idFunc, Func slugFunc) + { + return _id.HasValue + ? idFunc(_id.Value) + : slugFunc(_slug); + } + + public class IdentifierConvertor : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(int) || sourceType == typeof(string)) + return true; + return base.CanConvertFrom(context, sourceType); + } + + /// + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is int id) + return new Identifier(id); + if (value is not string slug) + return base.ConvertFrom(context, culture, value); + return int.TryParse(slug, out id) + ? new Identifier(id) + : new Identifier(slug); + } + } + } +} diff --git a/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs new file mode 100644 index 00000000..cda79146 --- /dev/null +++ b/src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs @@ -0,0 +1,40 @@ +// 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 Kyoo.Abstractions.Models.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Kyoo.Core.Controllers +{ + /// + /// The route constraint that goes with the . + /// + public class IdentifierRouteConstraint : IRouteConstraint + { + /// + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return values.ContainsKey(routeKey); + } + } +} diff --git a/src/Kyoo.Core/CoreModule.cs b/src/Kyoo.Core/CoreModule.cs index bdae8b38..5b0fe2cc 100644 --- a/src/Kyoo.Core/CoreModule.cs +++ b/src/Kyoo.Core/CoreModule.cs @@ -33,6 +33,7 @@ using Kyoo.Core.Tasks; using Kyoo.Database; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -145,6 +146,11 @@ namespace Kyoo.Core .AddDataAnnotations() .AddControllersAsServices() .AddApiExplorer() + .AddNewtonsoftJson(x => + { + x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); + x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); + }) .ConfigureApiBehaviorOptions(options => { options.SuppressMapClientErrors = true; @@ -157,12 +163,11 @@ namespace Kyoo.Core return new BadRequestObjectResult(new RequestError(errors)); }; }); - services.AddControllers() - .AddNewtonsoftJson(x => - { - x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl); - x.SerializerSettings.Converters.Add(new PeopleRoleConverter()); - }); + + services.Configure(x => + { + x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint)); + }); services.AddResponseCompression(x => { diff --git a/src/Kyoo.Core/Views/Helper/CrudApi.cs b/src/Kyoo.Core/Views/Helper/CrudApi.cs index 1c214da3..a90cfce1 100644 --- a/src/Kyoo.Core/Views/Helper/CrudApi.cs +++ b/src/Kyoo.Core/Views/Helper/CrudApi.cs @@ -85,42 +85,24 @@ namespace Kyoo.Core.Api } /// - /// Get by ID + /// Get item /// /// - /// Get a specific resource via it's ID. + /// Get a specific resource via it's ID or it's slug. /// - /// The ID of the resource to retrieve. + /// The ID or slug of the resource to retrieve. /// The retrieved resource. - /// A resource with the given ID does not exist. - [HttpGet("{id:int}")] + /// A resource with the given ID or slug does not exist. + [HttpGet("{identifier:id}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get(int id) + public async Task> Get(Identifier identifier) { - T ret = await _repository.GetOrDefault(id); - if (ret == null) - return NotFound(); - return ret; - } - - /// - /// Get by slug - /// - /// - /// Get a specific resource via it's slug (a unique, human readable identifier). - /// - /// The slug of the resource to retrieve. - /// The retrieved resource. - /// A resource with the given ID does not exist. - [HttpGet("{slug}")] - [PartialPermission(Kind.Read)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get(string slug) - { - T ret = await _repository.GetOrDefault(slug); + T ret = await identifier.Match( + id => _repository.GetOrDefault(id), + slug => _repository.GetOrDefault(slug) + ); if (ret == null) return NotFound(); return ret; @@ -256,50 +238,26 @@ namespace Kyoo.Core.Api } /// - /// Delete by ID + /// Delete an item /// /// - /// Delete one item via it's ID. + /// Delete one item via it's ID or it's slug. /// - /// The ID of the resource to delete. + /// The ID or slug of the resource to delete. /// The item has successfully been deleted. - /// No item could be found with the given id. - [HttpDelete("{id:int}")] + /// No item could be found with the given id or slug. + [HttpDelete("{identifier:id}")] [PartialPermission(Kind.Delete)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Delete(int id) + public async Task Delete(Identifier identifier) { try { - await _repository.Delete(id); - } - catch (ItemNotFoundException) - { - return NotFound(); - } - - return Ok(); - } - - /// - /// Delete by slug - /// - /// - /// Delete one item via it's slug (an unique, human-readable identifier). - /// - /// The slug of the resource to delete. - /// The item has successfully been deleted. - /// No item could be found with the given slug. - [HttpDelete("{slug}")] - [PartialPermission(Kind.Delete)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Delete(string slug) - { - try - { - await _repository.Delete(slug); + await identifier.Match( + id => _repository.Delete(id), + slug => _repository.Delete(slug) + ); } catch (ItemNotFoundException) { diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index 8bea787a..dfc4135f 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -19,9 +19,12 @@ using System; using System.Collections.Generic; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.DependencyInjection; +using NJsonSchema; +using NJsonSchema.Generation.TypeMappers; using NSwag; using NSwag.Generation.AspNetCore; using static Kyoo.Abstractions.Models.Utils.Constants; @@ -77,6 +80,11 @@ namespace Kyoo.Swagger return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute; return true; }); + options.SchemaGenerator.Settings.TypeMappers + .Add(new PrimitiveTypeMapper( + typeof(Identifier), + x => x.Type = JsonObjectType.String | JsonObjectType.Integer) + ); }); }