Skip to content

Commit

Permalink
Support Results
Browse files Browse the repository at this point in the history
  • Loading branch information
Javier García de la Noceda Argüelles committed Oct 2, 2024
1 parent c051ca6 commit 02ed394
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.Swagger;


#if NET7_0_OR_GREATER
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Controllers;
#endif

namespace Swashbuckle.AspNetCore.SwaggerGen
Expand Down Expand Up @@ -436,7 +439,7 @@ private async Task<OpenApiOperation> GenerateOpenApiOperationFromMetadataAsync(A
if (countOfParameters == 1)
{
var requestParameter = requestParameters.First();
content.Schema = GenerateSchemaIncludingFormFile(requestParameter, GenerateSchema(
content.Schema = GenerateSchemaIncludingFromFormWithNoReference(requestParameter, GenerateSchema(
requestParameter.ModelMetadata.ModelType,
schemaRepository,
requestParameter.PropertyInfo(),
Expand All @@ -449,7 +452,7 @@ private async Task<OpenApiOperation> GenerateOpenApiOperationFromMetadataAsync(A
content.Schema = new OpenApiSchema()
{
AllOf = requestParameters.Select(s =>
GenerateSchemaIncludingFormFile(s, GenerateSchema(
GenerateSchemaIncludingFromFormWithNoReference(s, GenerateSchema(
s.ModelMetadata.ModelType,
schemaRepository,
s.PropertyInfo(),
Expand All @@ -475,7 +478,7 @@ private async Task<OpenApiOperation> GenerateOpenApiOperationFromMetadataAsync(A
}
}

static OpenApiSchema GenerateSchemaIncludingFormFile(ApiParameterDescription apiParameterDescription, OpenApiSchema generatedSchema, OpenApiMediaType mediaType)
static OpenApiSchema GenerateSchemaIncludingFromFormWithNoReference(ApiParameterDescription apiParameterDescription, OpenApiSchema generatedSchema, OpenApiMediaType mediaType)
{
if (generatedSchema.Reference is null && apiParameterDescription.IsFromForm())
{
Expand Down Expand Up @@ -881,12 +884,51 @@ private OpenApiSchema GenerateSchemaFromFormParameters(
Required = new SortedSet<string>(requiredPropertyNames)
};
}
private static IList<ApiResponseType> GetResponseTypes(ApiDescription apiDescription)
{
#if NET7_0_OR_GREATER
var supportedResponseTypes = new List<ApiResponseType>(apiDescription.SupportedResponseTypes);
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
var returnType = UnwrapTask(controllerActionDescriptor.MethodInfo.ReturnType);
if (typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType))
{
var populateMetadataMethod = returnType.GetMethod("Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata", BindingFlags.Static | BindingFlags.NonPublic);
if (populateMetadataMethod != null)
{
var endpointBuilder = new MetadataEndpointBuilder();
populateMetadataMethod.Invoke(null, [controllerActionDescriptor.MethodInfo, endpointBuilder]);
var responseTypes = endpointBuilder.Metadata.Cast<IProducesResponseTypeMetadata>().ToList();
foreach (var responseType in responseTypes)
{
supportedResponseTypes.Add(
new ApiResponseType()
{
IsDefaultResponse = false,
Type = responseType.Type,
StatusCode = responseType.StatusCode,
ApiResponseFormats = responseType.ContentTypes.Select(contentType => new ApiResponseFormat { MediaType = contentType }).ToList()
});
}
}
}
}

static Type UnwrapTask(Type type)
=> type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)
? type.GetGenericArguments()[0]
: type;
return supportedResponseTypes;
#else
return apiDescription.SupportedResponseTypes;
#endif
}

private OpenApiResponses GenerateResponses(
ApiDescription apiDescription,
SchemaRepository schemaRepository)
{
var supportedResponseTypes = apiDescription.SupportedResponseTypes
var supportedResponseTypes = GetResponseTypes(apiDescription)
.DefaultIfEmpty(new ApiResponseType { StatusCode = 200 });

var responses = new OpenApiResponses();
Expand Down Expand Up @@ -1055,6 +1097,11 @@ private static string GenerateDescription(ApiDescription apiDescription) =>
?.OfType<IEndpointDescriptionMetadata>()
.Select(s => s.Description)
.LastOrDefault();

private sealed class MetadataEndpointBuilder : EndpointBuilder
{
public override Endpoint Build() => null!;
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@
],
"responses": {
"200": {
"description": "OK"
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LogLevel"
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures;

#if NET7_0_OR_GREATER
using Microsoft.AspNetCore.Http.HttpResults;
#endif

namespace Swashbuckle.AspNetCore.SwaggerGen.Test
{
public class FakeController
Expand Down Expand Up @@ -109,5 +113,8 @@ public void ActionHavingFromFormAttributeButNotWithIFormFile([FromForm] string p
{ }
public void ActionHavingFromFormAttributeWithSwaggerIgnore([FromForm] SwaggerIngoreAnnotatedType param1)
{ }
#if NET7_0_OR_GREATER
public Results<Ok<int>,NotFound> ActionHavingResults() => TypedResults.Ok(1);
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2321,6 +2321,27 @@ public void GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSt
Assert.Equal(ParameterStyle.Form, content.Value.Encoding["param"].Style);
}

#if NET7_0_OR_GREATER
[Fact]
public void GetSwagger_SetsResponse_IfActionHasHttpResult()
{
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionHavingResults), groupName: "v1", httpMethod: "POST", relativePath: "resource"),
]
);

var document = subject.GetSwagger("v1");
var responses = document.Paths["/resource"].Operations[OperationType.Post].Responses;
Assert.NotEmpty(responses);
Assert.NotEmpty(responses["200"].Content);
Assert.Equal("int32", responses["200"].Content.FirstOrDefault().Value.Schema.Format);
Assert.Empty(responses["404"].Content);
}
#endif

private static SwaggerGenerator Subject(
IEnumerable<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
Info: {
Title: Test API,
Version: V1
},
Paths: {
/resource: {
Operations: {
Post: {
Tags: [
{
Name: Fake,
UnresolvedReference: false
}
],
Responses: {
200: {
Description: OK,
Content: {
application/json: {
Schema: {
Type: integer,
Format: int32,
ReadOnly: false,
WriteOnly: false,
AdditionalPropertiesAllowed: true,
Nullable: false,
Deprecated: false,
UnresolvedReference: false
}
}
},
UnresolvedReference: false
},
404: {
Description: Not Found,
UnresolvedReference: false
}
},
Deprecated: false
}
},
UnresolvedReference: false
}
},
Components: {},
HashCode: 2568F195C8E7044994B2913208FC9DA1D22B1CD954855BB6B9F9659F8262B949DF11C0A191FBEFF713F9AB6FDCF9B092D311714661D8AA13D8153DEF3D8719D3
}
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,24 @@ public Task GetSwagger_GenerateConsumesSchemas_ForProvidedOpenApiOperationWithSt
return Verifier.Verify(document);
}

#if NET7_0_OR_GREATER
[Fact]
public Task GetSwagger_SetsResponse_IfActionHasHttpResult()
{
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionHavingResults), groupName: "v1", httpMethod: "POST", relativePath: "resource"),
]
);

var document = subject.GetSwagger("v1");

return Verifier.Verify(document);
}
#endif

private static SwaggerGenerator Subject(
IEnumerable<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
Expand Down
3 changes: 2 additions & 1 deletion test/WebSites/MvcWithNullable/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -28,7 +29,7 @@
public class EnumController : ControllerBase
{
[HttpGet]
public IActionResult Get(LogLevel? logLevel = LogLevel.Error) => Ok(new { logLevel });
public Results<Ok<LogLevel?>, NotFound> Get(LogLevel? logLevel = LogLevel.Error) => TypedResults.Ok(logLevel);
}

namespace MvcWithNullable
Expand Down

0 comments on commit 02ed394

Please sign in to comment.