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