Skip to content

Commit

Permalink
[IDP-1405] Support Forbidden and ForbiddenOfT in TypedResultExtensions (
Browse files Browse the repository at this point in the history
#14)

* [IDP-1405] Support Forbidden in TypedResultExtensions as a response type

* Added more tests to catch return cases
  • Loading branch information
heqianwang committed May 10, 2024
1 parent f0d0d93 commit 80454ba
Show file tree
Hide file tree
Showing 14 changed files with 516 additions and 24 deletions.
6 changes: 4 additions & 2 deletions src/Shared/HttpResultsStatusCodeTypeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ internal static class HttpResultsStatusCodeTypeHelpers
{"Microsoft.AspNetCore.Http.HttpResults.BadRequest", 400},
{"Microsoft.AspNetCore.Http.HttpResults.BadRequest`1", 400},
{"Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult", 401},
{"Workleap.Extensions.OpenAPI.TypedResult.Forbidden", 403},
{"Workleap.Extensions.OpenAPI.TypedResult.Forbidden`1", 403},
{"Microsoft.AspNetCore.Http.HttpResults.NotFound", 404},
{"Microsoft.AspNetCore.Http.HttpResults.NotFound`1", 404},
{"Microsoft.AspNetCore.Http.HttpResults.Conflict", 409},
{"Microsoft.AspNetCore.Http.HttpResults.Conflict`1", 409},
{"Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity", 422},
{"Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity`T", 422},
{"Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity`1", 422},
// Will be Supported in .NET 9
{"Microsoft.AspNetCore.Http.HttpResults.InternalServerError", 500},
{"Microsoft.AspNetCore.Http.HttpResults.InternalServerError`1", 500},
// Workleap's definition of the InternalServerError type result for other .NET versions
{"Workleap.Extensions.OpenAPI.TypedResult.InternalServerError", 500},
{"Workleap.Extensions.OpenAPI.TypedResult.InternalServerError`1", 500}
{"Workleap.Extensions.OpenAPI.TypedResult.InternalServerError`1", 500},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ private sealed class AnalyzerContext(Compilation compilation)
Add(204, "Microsoft.AspNetCore.Http.HttpResults.NoContent");
Add(400, "Microsoft.AspNetCore.Http.HttpResults.BadRequest");
Add(401, "Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult");
Add(403, "Workleap.Extensions.OpenAPI.TypedResult.Forbidden");
Add(404, "Microsoft.AspNetCore.Http.HttpResults.NotFound");
Add(409, "Microsoft.AspNetCore.Http.HttpResults.Conflict");
Add(422, "Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ private sealed class AnalyzerContext(Compilation compilation)
{
private INamedTypeSymbol? TaskOfTSymbol { get; } = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1");
private INamedTypeSymbol? ValueTaskOfTSymbol { get; } = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1");
private INamedTypeSymbol? ResultSymbol { get; } = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IResult");
private INamedTypeSymbol? ActionResultSymbol { get; } = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.IActionResult");
private INamedTypeSymbol? ResultInterfaceSymbol { get; } = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Http.IResult");
private INamedTypeSymbol? ActionResultInterfaceSymbol { get; } = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.IActionResult");
private INamedTypeSymbol? ActionResultSymbol { get; } = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.ActionResult");

public bool IsValid => this.ActionResultSymbol is not null || this.ResultSymbol is not null;
public bool IsValid => this.ActionResultInterfaceSymbol is not null || this.ResultInterfaceSymbol is not null;

public void ValidateEndpointResponseType(SymbolAnalysisContext context)
{
Expand Down Expand Up @@ -70,7 +71,9 @@ public void ValidateEndpointResponseType(SymbolAnalysisContext context)

private bool IsWeaklyTypedResults(ITypeSymbol currentClassSymbol)
{
return SymbolEqualityComparer.Default.Equals(currentClassSymbol, this.ResultSymbol) || SymbolEqualityComparer.Default.Equals(currentClassSymbol, this.ActionResultSymbol);
return SymbolEqualityComparer.Default.Equals(currentClassSymbol, this.ResultInterfaceSymbol)
|| SymbolEqualityComparer.Default.Equals(currentClassSymbol, this.ActionResultInterfaceSymbol)
|| SymbolEqualityComparer.Default.Equals(currentClassSymbol, this.ActionResultSymbol);
}
}
}
7 changes: 7 additions & 0 deletions src/Workleap.Extensions.OpenAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#nullable enable
static Workleap.Extensions.OpenAPI.OpenApiServiceCollectionExtensions.ConfigureOpenApiGeneration(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
static Workleap.Extensions.OpenAPI.TypedResult.TypedResultsExtensions.Forbidden() -> Workleap.Extensions.OpenAPI.TypedResult.Forbidden!
static Workleap.Extensions.OpenAPI.TypedResult.TypedResultsExtensions.Forbidden<T>(T? error) -> Workleap.Extensions.OpenAPI.TypedResult.Forbidden<T>!
static Workleap.Extensions.OpenAPI.TypedResult.TypedResultsExtensions.InternalServerError() -> Workleap.Extensions.OpenAPI.TypedResult.InternalServerError!
static Workleap.Extensions.OpenAPI.TypedResult.TypedResultsExtensions.InternalServerError<T>(T? error) -> Workleap.Extensions.OpenAPI.TypedResult.InternalServerError<T>!
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder.GenerateMissingOperationId() -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
Workleap.Extensions.OpenAPI.OpenApiServiceCollectionExtensions
Workleap.Extensions.OpenAPI.TypedResult.Forbidden
Workleap.Extensions.OpenAPI.TypedResult.Forbidden.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Workleap.Extensions.OpenAPI.TypedResult.Forbidden<TValue>
Workleap.Extensions.OpenAPI.TypedResult.Forbidden<TValue>.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Workleap.Extensions.OpenAPI.TypedResult.Forbidden<TValue>.Value.get -> TValue?
Workleap.Extensions.OpenAPI.TypedResult.InternalServerError
Workleap.Extensions.OpenAPI.TypedResult.InternalServerError.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Workleap.Extensions.OpenAPI.TypedResult.InternalServerError<TValue>
Expand Down
69 changes: 69 additions & 0 deletions src/Workleap.Extensions.OpenAPI/TypedResult/FobiddenOfT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Net.Mime;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;

namespace Workleap.Extensions.OpenAPI.TypedResult;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with Forbidden (403) status code.
/// </summary>
/// <typeparam name="TValue">The type of error object that will be JSON serialized to the response body.</typeparam>
#pragma warning disable SA1649 // File name should match first type name - This is a Generic class.
public sealed class Forbidden<TValue> : IResult, IEndpointMetadataProvider, IStatusCodeHttpResult, IValueHttpResult, IValueHttpResult<TValue>
{
#pragma warning restore SA1649
/// <summary>
/// Initializes a new instance of the <see cref="Forbidden"/> class with the values
/// provided.
/// </summary>
/// <param name="error">The error content to format in the entity body.</param>
internal Forbidden(TValue? error)
{
this.Value = error;
if (this.Value is ProblemDetails problemDetails)
{
problemDetails.Type = "https://tools.ietf.org/html/rfc9110#section-15.5.4";
problemDetails.Title = "Forbidden";
}
}

/// <summary>
/// Gets the object result.
/// </summary>
public TValue? Value { get; }

object? IValueHttpResult.Value => this.Value;

/// <summary>
/// Gets the HTTP status code: <see cref="StatusCodes.Status403Forbidden"/>
/// </summary>
private static int StatusCode => StatusCodes.Status403Forbidden;

int? IStatusCodeHttpResult.StatusCode => StatusCode;

private static readonly string[] ContentTypes = new[] { "application/json" };

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);
httpContext.Response.ContentType = MediaTypeNames.Application.Json;
httpContext.Response.StatusCode = StatusCode;

return httpContext.Response.WriteAsJsonAsync(
this.Value);
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(method);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(TValue), ContentTypes));
}
}
46 changes: 46 additions & 0 deletions src/Workleap.Extensions.OpenAPI/TypedResult/Forbidden.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;

namespace Workleap.Extensions.OpenAPI.TypedResult;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with Forbidden (403) status code.
/// </summary>
public sealed class Forbidden : IResult, IEndpointMetadataProvider, IStatusCodeHttpResult
{
/// <summary>
/// Initializes a new instance of the <see cref="Forbidden"/> class with the values
/// provided.
/// </summary>
internal Forbidden()
{
}

/// <summary>
/// Gets the HTTP status code: <see cref="StatusCodes.Status403Forbidden"/>
/// </summary>
private int StatusCode => StatusCodes.Status403Forbidden;

int? IStatusCodeHttpResult.StatusCode => this.StatusCode;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);
httpContext.Response.StatusCode = this.StatusCode;

return Task.CompletedTask;
}

/// <inheritdoc/>
static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
ArgumentNullException.ThrowIfNull(method);
ArgumentNullException.ThrowIfNull(builder);

builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status403Forbidden, typeof(void)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

namespace Workleap.Extensions.OpenAPI.TypedResult;

/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with Internal Server Error (500) status code.
/// </summary>
/// <summary>
/// An <see cref="IResult"/> that on execution will write an object to the response
/// with Internal Server Error (500) status code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ public static class TypedResultsExtensions
{
public static InternalServerError InternalServerError() => new();
public static InternalServerError<T> InternalServerError<T>(T? error) => new(error);

public static Forbidden Forbidden() => new();
public static Forbidden<T> Forbidden<T>(T? error) => new(error);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,61 @@ public Ok<TypedResultExample> TypeResultWithOnlyOnePath(int id)
}

[HttpGet]
[Route("/withNoAnnotationForAcceptedAndUnprocessableResponse")]
public Results<Ok<TypedResultExample>, Accepted<TypedResultExample>, UnprocessableEntity> TypedResultWithNoAnnotationForAcceptedAndUnprocessableResponse(int id)
[Route("/withNoAnnotationForAcceptedAndUnprocessableResponseNoType")]
public Results<Ok<TypedResultExample>, Accepted, UnprocessableEntity> TypedResultWithNoAnnotationForAcceptedAndUnprocessableResponseNoType(int id)
{
return id switch
{
< 0 => TypedResults.UnprocessableEntity(),
0 => TypedResults.Ok(new TypedResultExample("Example")),
_ => TypedResults.Accepted("hardcoded uri", new TypedResultExample("Example"))
_ => TypedResults.Accepted("hardcoded uri")
};
}

[HttpGet]
[Route("/withNoAnnotationForAcceptedAndUnprocessableResponseWithType")]
public Results<Ok<TypedResultExample>, Accepted<TypedResultExample>, UnprocessableEntity<TypedResultExample>> TypedResultWithNoAnnotationForAcceptedAndUnprocessableResponseWithType(int id)
{
return id switch
{
< 0 => TypedResults.UnprocessableEntity(new TypedResultExample("Example")),
0 => TypedResults.Ok(new TypedResultExample("Example")),
_ => TypedResults.Accepted("hardcoded uri", new TypedResultExample("example"))
};
}

[HttpGet]
[Route("/withNoAnnotationForCreatedAndConflictNoType")]
public Results<Ok<TypedResultExample>, Created, Conflict> TypedResultWithNoAnnotationForCreatedAndConflictNoType(int id)
{
return id switch
{
< 0 => TypedResults.Conflict(),
0 => TypedResults.Ok(new TypedResultExample("Example")),
_ => TypedResults.Created()
};
}

[HttpGet]
[Route("/withNoAnnotationForCreatedAndConflictWithType")]
public Results<Ok<TypedResultExample>, Created<string>, Conflict<string>> TypedResultWithNoAnnotationForCreatedAndConflictWithType(int id)
{
return id switch
{
< 0 => TypedResults.Conflict("Conflict"),
0 => TypedResults.Ok(new TypedResultExample("Example")),
_ => TypedResults.Created("hardcoded uri", "Created")
};
}

[HttpGet]
[Route("/withNoAnnotationForNoContentAndUnauthorized")]
public Results<NoContent, UnauthorizedHttpResult> TypedResultWithNoAnnotationForNoContentAndUnauthorized(int id)
{
return id switch
{
< 0 => TypedResults.NoContent(),
_ => TypedResults.Unauthorized()
};
}

Expand All @@ -73,11 +120,24 @@ public Ok<ProblemDetails> TypedResultWithSwaggerResponseAnnotation()
}

[HttpGet]
[Route("/withExceptionsWithExtensions")]
public Results<Ok<TypedResultExample>, InternalServerError<string>> TypedResultWithExceptionsWithExtensions(int id)
[Route("/withExceptionsNoType")]
public Results<Ok<TypedResultExample>, Forbidden, InternalServerError> TypedResultWithExceptionsNoType(int id)
{
return id switch
{
0 => TypedResultsExtensions.Forbidden(),
< 0 => TypedResultsExtensions.InternalServerError(),
_ => TypedResults.Ok(new TypedResultExample("Example"))
};
}

[HttpGet]
[Route("/withExceptionsWithType")]
public Results<Ok<TypedResultExample>, Forbidden<string>, InternalServerError<string>> TypedResultWithExceptionsWithType(int id)
{
return id switch
{
0 => TypedResultsExtensions.Forbidden("Forbidden"),
< 0 => TypedResultsExtensions.InternalServerError("An error occured when processing the request."),
_ => TypedResults.Ok(new TypedResultExample("Example"))
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="Workleap.OpenApi.MSBuild" Version="0.6.0">
<PackageReference Include="Workleap.OpenApi.MSBuild" Version="0.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading

0 comments on commit 80454ba

Please sign in to comment.