mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
API: Starting to merge id/slug routes using a new Identifier
This commit is contained in:
parent
f0e9054b36
commit
41cbc50940
112
src/Kyoo.Abstractions/Models/Utils/Identifier.cs
Normal file
112
src/Kyoo.Abstractions/Models/Utils/Identifier.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs
Normal file
40
src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user