Skip to content

Commit

Permalink
Fix ProducesResponseType support and improve accuracy of annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
Hawxy authored and jeremydmiller committed Jan 21, 2024
1 parent 3e606d3 commit 60197d7
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 7 deletions.
40 changes: 34 additions & 6 deletions src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Routing;
Expand Down Expand Up @@ -118,25 +120,51 @@ public ApiDescription CreateApiDescription(string httpMethod)
return apiDescription;
}

private sealed record NormalizedResponseMetadata(int StatusCode, Type? Type, IEnumerable<string> ContentTypes)
{
// if an attribute doesn't specific the content type, conform with OpenAPI internals and infer.
public IEnumerable<string> GetContentTypes()
{
if (ContentTypes.Any())
return ContentTypes;
if (Type == typeof(string))
return new[] { "text/plain" };
if (Type == typeof(ProblemDetails))
return new[] { "application/problem+json" };
return new[] { "application/json" };
}
}
private void fillResponseTypes(ApiDescription apiDescription)
{
var responseTypes = Endpoint.Metadata.OfType<IProducesResponseTypeMetadata>().GroupBy(x => x.StatusCode)
.ToArray();
var attributeMetadata = Endpoint!.Metadata
.OfType<IApiResponseMetadataProvider>()
.Select(x =>
{
var attributeContentTypes = new MediaTypeCollection();
x.SetContentTypes(attributeContentTypes);
return new NormalizedResponseMetadata(x.StatusCode, x.Type, attributeContentTypes);
});

var responseMetadata = Endpoint.Metadata
.OfType<IProducesResponseTypeMetadata>()
.Select(x=> new NormalizedResponseMetadata(x.StatusCode, x.Type, x.ContentTypes));

// Attributes take priority over computed metadata
var responseTypes = attributeMetadata.Concat(responseMetadata).GroupBy(x => x.StatusCode);

foreach (var responseTypeMetadata in responseTypes)
{
var responseType = responseTypeMetadata.FirstOrDefault(x => x.Type != typeof(void))?.Type;


var apiResponseType = new ApiResponseType
{
StatusCode = responseTypeMetadata.Key,
ModelMetadata = responseType == null ? null : new EndpointModelMetadata(responseType),
IsDefaultResponse = false, // this seems to mean "no explicit response", so never set this to true
ApiResponseFormats = new List<ApiResponseFormat>(responseTypeMetadata.SelectMany(x => x.ContentTypes)
ApiResponseFormats = responseTypeMetadata.First().GetContentTypes()
.Select(x => new ApiResponseFormat
{
MediaType = x
})),
}).ToList(),
Type = responseType
};

Expand Down
1 change: 0 additions & 1 deletion src/Http/Wolverine.Http/Wolverine.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
<DebugType>portable</DebugType>
<PackageId>WolverineFx.Http</PackageId>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<OutputType>library</OutputType>
</PropertyGroup>
Expand Down
19 changes: 19 additions & 0 deletions src/Http/WolverineWebApi/OpenApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Wolverine;
using Wolverine.Http;
Expand Down Expand Up @@ -32,6 +33,22 @@ public Reservation GetJson()
{
return new Reservation();
}

[ExpectMatch(OperationType.Get, "/expect/getresult")]
[ProducesResponseType(typeof(ProblemDetails), 400)]
[ProducesResponseType(typeof(string), 200)]
[WolverineGet("/openapi/getresult")]
public IResult GetResult()
{
return Results.Ok("a string");
}

[ExpectMatch(OperationType.Get, "/expect/getresulttyped")]
[WolverineGet("/openapi/getresulttyped")]
public Results<Ok<Reservation>, ProblemHttpResult> GetResultTyped()
{
return TypedResults.Ok(new Reservation());
}

[ExpectMatch(OperationType.Get, "/expect/nocontent")]
[WolverineGet("/openapi/nocontent")]
Expand All @@ -54,6 +71,8 @@ public static void BuildComparisonRoutes(WebApplication app)
app.MapPost("/expect/nocontent", () => TypedResults.NoContent());
app.MapGet("/expect/string", () => "hello, world");
app.MapGet("/expect/json", () => TypedResults.Json(new Reservation())).Produces(404);
app.MapGet("/expect/getresult", () => Results.Ok("a string")).ProducesProblem(400).Produces(404).Produces<string>(contentType: "text/plain");
app.MapGet("/expect/getresulttyped", Results<Ok<Reservation>, ProblemHttpResult> () => TypedResults.Ok(new Reservation()));
}
}

Expand Down

0 comments on commit 60197d7

Please sign in to comment.