API: Starting to merge id/slug routes using a new Identifier

This commit is contained in:
Zoe Roux 2021-09-21 16:42:49 +02:00
parent f0e9054b36
commit 41cbc50940
5 changed files with 191 additions and 68 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
using System;
using System.ComponentModel;
using System.Globalization;
using JetBrains.Annotations;
namespace Kyoo.Abstractions.Models.Utils
{
/// <summary>
/// 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 <see cref="IResource"/>.
/// </summary>
[TypeConverter(typeof(IdentifierConvertor))]
public class Identifier
{
/// <summary>
/// The ID of the resource or null if the slug is specified.
/// </summary>
private readonly int? _id;
/// <summary>
/// The slug of the resource or null if the id is specified.
/// </summary>
private readonly string _slug;
/// <summary>
/// Create a new <see cref="Identifier"/> for the given id.
/// </summary>
/// <param name="id">The id of the resource.</param>
public Identifier(int id)
{
_id = id;
}
/// <summary>
/// Create a new <see cref="Identifier"/> for the given slug.
/// </summary>
/// <param name="slug">The slug of the resource.</param>
public Identifier([NotNull] string slug)
{
if (slug == null)
throw new ArgumentNullException(nameof(slug));
_slug = slug;
}
/// <summary>
/// Pattern match out of the identifier to a resource.
/// </summary>
/// <param name="idFunc">The function to match the ID to a type <typeparamref name="T"/>.</param>
/// <param name="slugFunc">The function to match the slug to a type <typeparamref name="T"/>.</param>
/// <typeparam name="T">The return type that will be converted to from an ID or a slug.</typeparam>
/// <returns>
/// The result of the <paramref name="idFunc"/> or <paramref name="slugFunc"/> depending on the pattern.
/// </returns>
/// <example>
/// Example usage:
/// <code lang="csharp">
/// T ret = await identifier.Match(
/// id => _repository.GetOrDefault(id),
/// slug => _repository.GetOrDefault(slug)
/// );
/// </code>
/// </example>
public T Match<T>(Func<int, T> idFunc, Func<string, T> slugFunc)
{
return _id.HasValue
? idFunc(_id.Value)
: slugFunc(_slug);
}
public class IdentifierConvertor : TypeConverter
{
/// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(int) || sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
/// <inheritdoc />
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);
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
using Kyoo.Abstractions.Models.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// The route constraint that goes with the <see cref="Identifier"/>.
/// </summary>
public class IdentifierRouteConstraint : IRouteConstraint
{
/// <inheritdoc />
public bool Match(HttpContext httpContext,
IRouter route,
string routeKey,
RouteValueDictionary values,
RouteDirection routeDirection)
{
return values.ContainsKey(routeKey);
}
}
}

View File

@ -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,11 +163,10 @@ namespace Kyoo.Core
return new BadRequestObjectResult(new RequestError(errors));
};
});
services.AddControllers()
.AddNewtonsoftJson(x =>
services.Configure<RouteOptions>(x =>
{
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl);
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
});
services.AddResponseCompression(x =>

View File

@ -85,42 +85,24 @@ namespace Kyoo.Core.Api
}
/// <summary>
/// Get by ID
/// Get item
/// </summary>
/// <remarks>
/// Get a specific resource via it's ID.
/// Get a specific resource via it's ID or it's slug.
/// </remarks>
/// <param name="id">The ID of the resource to retrieve.</param>
/// <param name="identifier">The ID or slug of the resource to retrieve.</param>
/// <returns>The retrieved resource.</returns>
/// <response code="404">A resource with the given ID does not exist.</response>
[HttpGet("{id:int}")]
/// <response code="404">A resource with the given ID or slug does not exist.</response>
[HttpGet("{identifier:id}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Get(int id)
public async Task<ActionResult<T>> Get(Identifier identifier)
{
T ret = await _repository.GetOrDefault(id);
if (ret == null)
return NotFound();
return ret;
}
/// <summary>
/// Get by slug
/// </summary>
/// <remarks>
/// Get a specific resource via it's slug (a unique, human readable identifier).
/// </remarks>
/// <param name="slug">The slug of the resource to retrieve.</param>
/// <returns>The retrieved resource.</returns>
/// <response code="404">A resource with the given ID does not exist.</response>
[HttpGet("{slug}")]
[PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> 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
}
/// <summary>
/// Delete by ID
/// Delete an item
/// </summary>
/// <remarks>
/// Delete one item via it's ID.
/// Delete one item via it's ID or it's slug.
/// </remarks>
/// <param name="id">The ID of the resource to delete.</param>
/// <param name="identifier">The ID or slug of the resource to delete.</param>
/// <returns>The item has successfully been deleted.</returns>
/// <response code="404">No item could be found with the given id.</response>
[HttpDelete("{id:int}")]
/// <response code="404">No item could be found with the given id or slug.</response>
[HttpDelete("{identifier:id}")]
[PartialPermission(Kind.Delete)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(int id)
public async Task<IActionResult> Delete(Identifier identifier)
{
try
{
await _repository.Delete(id);
}
catch (ItemNotFoundException)
{
return NotFound();
}
return Ok();
}
/// <summary>
/// Delete by slug
/// </summary>
/// <remarks>
/// Delete one item via it's slug (an unique, human-readable identifier).
/// </remarks>
/// <param name="slug">The slug of the resource to delete.</param>
/// <returns>The item has successfully been deleted.</returns>
/// <response code="404">No item could be found with the given slug.</response>
[HttpDelete("{slug}")]
[PartialPermission(Kind.Delete)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(string slug)
{
try
{
await _repository.Delete(slug);
await identifier.Match(
id => _repository.Delete(id),
slug => _repository.Delete(slug)
);
}
catch (ItemNotFoundException)
{

View File

@ -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)
);
});
}