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 Kyoo.Database;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -145,6 +146,11 @@ namespace Kyoo.Core
|
|||||||
.AddDataAnnotations()
|
.AddDataAnnotations()
|
||||||
.AddControllersAsServices()
|
.AddControllersAsServices()
|
||||||
.AddApiExplorer()
|
.AddApiExplorer()
|
||||||
|
.AddNewtonsoftJson(x =>
|
||||||
|
{
|
||||||
|
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl);
|
||||||
|
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
|
||||||
|
})
|
||||||
.ConfigureApiBehaviorOptions(options =>
|
.ConfigureApiBehaviorOptions(options =>
|
||||||
{
|
{
|
||||||
options.SuppressMapClientErrors = true;
|
options.SuppressMapClientErrors = true;
|
||||||
@ -157,12 +163,11 @@ namespace Kyoo.Core
|
|||||||
return new BadRequestObjectResult(new RequestError(errors));
|
return new BadRequestObjectResult(new RequestError(errors));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
services.AddControllers()
|
|
||||||
.AddNewtonsoftJson(x =>
|
services.Configure<RouteOptions>(x =>
|
||||||
{
|
{
|
||||||
x.SerializerSettings.ContractResolver = new JsonPropertyIgnorer(publicUrl);
|
x.ConstraintMap.Add("id", typeof(IdentifierRouteConstraint));
|
||||||
x.SerializerSettings.Converters.Add(new PeopleRoleConverter());
|
});
|
||||||
});
|
|
||||||
|
|
||||||
services.AddResponseCompression(x =>
|
services.AddResponseCompression(x =>
|
||||||
{
|
{
|
||||||
|
@ -85,42 +85,24 @@ namespace Kyoo.Core.Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get by ID
|
/// Get item
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Get a specific resource via it's ID.
|
/// Get a specific resource via it's ID or it's slug.
|
||||||
/// </remarks>
|
/// </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>
|
/// <returns>The retrieved resource.</returns>
|
||||||
/// <response code="404">A resource with the given ID does not exist.</response>
|
/// <response code="404">A resource with the given ID or slug does not exist.</response>
|
||||||
[HttpGet("{id:int}")]
|
[HttpGet("{identifier:id}")]
|
||||||
[PartialPermission(Kind.Read)]
|
[PartialPermission(Kind.Read)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[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);
|
T ret = await identifier.Match(
|
||||||
if (ret == null)
|
id => _repository.GetOrDefault(id),
|
||||||
return NotFound();
|
slug => _repository.GetOrDefault(slug)
|
||||||
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);
|
|
||||||
if (ret == null)
|
if (ret == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
return ret;
|
return ret;
|
||||||
@ -256,50 +238,26 @@ namespace Kyoo.Core.Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete by ID
|
/// Delete an item
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Delete one item via it's ID.
|
/// Delete one item via it's ID or it's slug.
|
||||||
/// </remarks>
|
/// </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>
|
/// <returns>The item has successfully been deleted.</returns>
|
||||||
/// <response code="404">No item could be found with the given id.</response>
|
/// <response code="404">No item could be found with the given id or slug.</response>
|
||||||
[HttpDelete("{id:int}")]
|
[HttpDelete("{identifier:id}")]
|
||||||
[PartialPermission(Kind.Delete)]
|
[PartialPermission(Kind.Delete)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> Delete(int id)
|
public async Task<IActionResult> Delete(Identifier identifier)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _repository.Delete(id);
|
await identifier.Match(
|
||||||
}
|
id => _repository.Delete(id),
|
||||||
catch (ItemNotFoundException)
|
slug => _repository.Delete(slug)
|
||||||
{
|
);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
catch (ItemNotFoundException)
|
catch (ItemNotFoundException)
|
||||||
{
|
{
|
||||||
|
@ -19,9 +19,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
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 NJsonSchema;
|
||||||
|
using NJsonSchema.Generation.TypeMappers;
|
||||||
using NSwag;
|
using NSwag;
|
||||||
using NSwag.Generation.AspNetCore;
|
using NSwag.Generation.AspNetCore;
|
||||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||||
@ -77,6 +80,11 @@ namespace Kyoo.Swagger
|
|||||||
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
|
return ctx.ApiDescription.ActionDescriptor.AttributeRouteInfo?.Order != AlternativeRoute;
|
||||||
return true;
|
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