Better support NoContent results in authorities

This commit is contained in:
Jordan Dominion
2024-09-15 17:17:35 -04:00
parent f7969691ce
commit 3ffbb74cea
5 changed files with 89 additions and 20 deletions
@@ -53,6 +53,16 @@ namespace Tgstation.Server.Host.Authority.Core
new ErrorMessageResponse(),
HttpFailureResponse.Unauthorized);
/// <summary>
/// Generates a <see cref="HttpFailureResponse.Forbidden"/> type <see cref="AuthorityResponse{TResult}"/>.
/// </summary>
/// <typeparam name="TResult">The <see cref="Type"/> of the <see cref="AuthorityResponse{TResult}.Result"/>.</typeparam>
/// <returns>A new, errored <see cref="AuthorityResponse{TResult}"/>.</returns>
protected static AuthorityResponse<TResult> Gone<TResult>()
=> new(
new ErrorMessageResponse(),
HttpFailureResponse.Gone);
/// <summary>
/// Generates a <see cref="HttpFailureResponse.Forbidden"/> type <see cref="AuthorityResponse{TResult}"/>.
/// </summary>
@@ -73,6 +83,17 @@ namespace Tgstation.Server.Host.Authority.Core
new ErrorMessageResponse(),
HttpFailureResponse.NotFound);
/// <summary>
/// Generates a <see cref="HttpFailureResponse.Conflict"/> type <see cref="AuthorityResponse{TResult}"/>.
/// </summary>
/// <typeparam name="TResult">The <see cref="Type"/> of the <see cref="AuthorityResponse{TResult}.Result"/>.</typeparam>
/// <param name="errorCode">The <see cref="ErrorCode"/>.</param>
/// <returns>A new, errored <see cref="AuthorityResponse{TResult}"/>.</returns>
protected static AuthorityResponse<TResult> Conflict<TResult>(ErrorCode errorCode)
=> new(
new ErrorMessageResponse(errorCode),
HttpFailureResponse.Conflict);
/// <summary>
/// Initializes a new instance of the <see cref="AuthorityBase"/> class.
/// </summary>
@@ -13,10 +13,16 @@ namespace Tgstation.Server.Host.Authority.Core
public sealed class AuthorityResponse<TResult> : AuthorityResponse
{
/// <inheritdoc />
[MemberNotNullWhen(true, nameof(Result))]
[MemberNotNullWhen(true, nameof(SuccessResponse))]
[MemberNotNullWhen(true, nameof(IsNoContent))]
public override bool Success => base.Success;
/// <summary>
/// Checks if a the <see cref="AuthorityResponse{TResult}"/> is a no content result. Only set on <see cref="Success"/>.
/// </summary>
[MemberNotNullWhen(false, nameof(Result))]
[MemberNotNullWhen(false, nameof(Result))]
public bool? IsNoContent => Success ? Result == null : null;
/// <summary>
/// The success <typeparamref name="TResult"/>.
/// </summary>
@@ -47,5 +53,13 @@ namespace Tgstation.Server.Host.Authority.Core
Result = result ?? throw new ArgumentNullException(nameof(result));
SuccessResponse = httpResponse;
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthorityResponse{TResult}"/> class.
/// </summary>
/// <remarks>This generates an HTTP 204 response.</remarks>
public AuthorityResponse()
{
}
}
}
@@ -13,11 +13,12 @@ namespace Tgstation.Server.Host.Authority.Core
/// Throws a <see cref="ErrorMessageException"/> for errored <paramref name="authorityResponse"/>s.
/// </summary>
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/>.</param>
static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse)
/// <param name="errorOnMissing">If an error should be raised for <see cref="HttpFailureResponse.NotFound"/> and <see cref="HttpFailureResponse.Gone"/> failures.</param>
static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse, bool errorOnMissing)
{
if (authorityResponse.Success
|| authorityResponse.FailureResponse.Value == HttpFailureResponse.NotFound
|| authorityResponse.FailureResponse.Value == HttpFailureResponse.Gone)
|| ((authorityResponse.FailureResponse.Value == HttpFailureResponse.NotFound
|| authorityResponse.FailureResponse.Value == HttpFailureResponse.Gone) && !errorOnMissing))
return;
var fallbackString = authorityResponse.FailureResponse.ToString()!;
@@ -39,33 +40,41 @@ namespace Tgstation.Server.Host.Authority.Core
ArgumentNullException.ThrowIfNull(authorityInvoker);
var authorityResponse = await authorityInvoker(Authority);
ThrowGraphQLErrorIfNecessary(authorityResponse);
ThrowGraphQLErrorIfNecessary(authorityResponse, true);
}
/// <inheritdoc />
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TApiModel : default
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
var authorityResponse = await authorityInvoker(Authority);
ThrowGraphQLErrorIfNecessary(authorityResponse);
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
return authorityResponse.Result;
}
/// <inheritdoc />
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
async ValueTask<TApiModel?> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TApiModel : default
{
ArgumentNullException.ThrowIfNull(authorityInvoker);
var authorityResponse = await authorityInvoker(Authority);
ThrowGraphQLErrorIfNecessary(authorityResponse);
ThrowGraphQLErrorIfNecessary(authorityResponse, false);
var result = authorityResponse.Result;
if (result == null)
return default;
return result.ToApi();
}
/// <inheritdoc />
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeAllowMissing<TResult, TApiModel>(authorityInvoker)!;
/// <inheritdoc />
ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
=> ((IGraphQLAuthorityInvoker<TAuthority>)this).InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(authorityInvoker)!;
}
}
@@ -14,18 +14,22 @@ namespace Tgstation.Server.Host.Authority.Core
where TAuthority : IAuthority
{
/// <summary>
/// Create an <see cref="IActionResult"/> for a given successfuly <paramref name="authorityResponse"/> API <paramref name="result"/>.
/// Create an <see cref="IActionResult"/> for a given successfuly<paramref name="authorityResponse"/>.
/// </summary>
/// <param name="controller">The <see cref="ApiController"/> to use.</param>
/// <param name="result">The resulting <typeparamref name="TApiModel"/> from the <paramref name="authorityResponse"/>.</param>
/// <param name="resultTransformer">A <see cref="Func{T, TResult}"/> transforming the <typeparamref name="TResult"/> from the <paramref name="authorityResponse"/> into the <typeparamref name="TApiModel"/>.</param>
/// <param name="authorityResponse">The <see cref="AuthorityResponse{TResult}"/>.</param>
/// <returns>An <see cref="IActionResult"/> for the <paramref name="authorityResponse"/>.</returns>
/// <typeparam name="TResult">The result <see cref="Type"/> returned in the <paramref name="authorityResponse"/>.</typeparam>
/// <typeparam name="TApiModel">The REST API result model built from <paramref name="authorityResponse"/>.</typeparam>
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(ApiController controller, TApiModel result, AuthorityResponse<TResult> authorityResponse)
static IActionResult CreateSuccessfulActionResult<TResult, TApiModel>(ApiController controller, Func<TResult, TApiModel> resultTransformer, AuthorityResponse<TResult> authorityResponse)
where TApiModel : notnull
{
if (authorityResponse.IsNoContent!.Value)
return controller.NoContent();
var successResponse = authorityResponse.SuccessResponse;
var result = resultTransformer(authorityResponse.Result!);
return successResponse switch
{
HttpSuccessResponse.Ok => controller.Json(result),
@@ -95,8 +99,7 @@ namespace Tgstation.Server.Host.Authority.Core
if (erroredResult != null)
return erroredResult;
var result = authorityResponse.Result!;
return CreateSuccessfulActionResult(controller, result, authorityResponse);
return CreateSuccessfulActionResult(controller, result => result, authorityResponse);
}
/// <inheritdoc />
@@ -110,9 +113,7 @@ namespace Tgstation.Server.Host.Authority.Core
if (erroredResult != null)
return erroredResult;
var result = authorityResponse.Result!;
var apiModel = result.ToApi();
return CreateSuccessfulActionResult(controller, apiModel, authorityResponse);
return CreateSuccessfulActionResult(controller, result => result.ToApi(), authorityResponse);
}
}
}
@@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Authority
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<TApiModel?> Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
ValueTask<TApiModel?> InvokeAllowMissing<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TResult : TApiModel
where TApiModel : notnull;
@@ -39,7 +39,31 @@ namespace Tgstation.Server.Host.Authority
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<TApiModel?> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
ValueTask<TApiModel?> InvokeTransformableAllowMissing<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
where TApiModel : notnull
where TTransformer : ITransformer<TResult, TApiModel>, new();
/// <summary>
/// Invoke a <typeparamref name="TAuthority"/> method and get the non-nullable result.
/// </summary>
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<TApiModel> Invoke<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TResult : TApiModel
where TApiModel : notnull;
/// <summary>
/// Invoke a <typeparamref name="TAuthority"/> method and get the non-nullable result.
/// </summary>
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
where TApiModel : notnull
where TTransformer : ITransformer<TResult, TApiModel>, new();