Skip to content

Commit

Permalink
[IDP-1378] Support Http response codes with uri input parameters (#10)
Browse files Browse the repository at this point in the history
* [IDP-1378] Support Http response codes with uri input parameters
  • Loading branch information
heqianwang authored May 6, 2024
1 parent 690b20e commit 5398351
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 47 deletions.
32 changes: 32 additions & 0 deletions src/Shared/HttpResultsStatusCodeTypeHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
internal static class HttpResultsStatusCodeTypeHelpers
{
public static Dictionary<string, int> HttpResultTypeToStatusCodes { get; } = new()
{
{"Microsoft.AspNetCore.Http.HttpResults.Ok", 200},
{"Microsoft.AspNetCore.Http.HttpResults.Ok`1", 200},
{"Microsoft.AspNetCore.Http.HttpResults.Created", 201},
{"Microsoft.AspNetCore.Http.HttpResults.Created`1", 201},
{"Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute", 201},
{"Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute`1", 201},
{"Microsoft.AspNetCore.Http.HttpResults.Accepted", 202},
{"Microsoft.AspNetCore.Http.HttpResults.Accepted`1", 202},
{"Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute", 202},
{"Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute`1", 202},
{"Microsoft.AspNetCore.Http.HttpResults.NoContent", 204},
{"Microsoft.AspNetCore.Http.HttpResults.BadRequest", 400},
{"Microsoft.AspNetCore.Http.HttpResults.BadRequest`1", 400},
{"Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult", 401},
{"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},
// 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}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,43 +61,17 @@ private sealed class AnalyzerContext(Compilation compilation)
private static Dictionary<ITypeSymbol, int> InitializeHttpResultStatusCodeMap(Compilation compilation)
{
var dictionary = new Dictionary<ITypeSymbol, int>(SymbolEqualityComparer.Default);
Add("Microsoft.AspNetCore.Http.HttpResults.Ok", 200);
Add("Microsoft.AspNetCore.Http.HttpResults.Ok`1", 200);
Add("Microsoft.AspNetCore.Http.HttpResults.Created", 201);
Add("Microsoft.AspNetCore.Http.HttpResults.Created`1", 201);
Add("Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute", 201);
Add("Microsoft.AspNetCore.Http.HttpResults.CreatedAtRoute`1", 201);
Add("Microsoft.AspNetCore.Http.HttpResults.Accepted", 202);
Add("Microsoft.AspNetCore.Http.HttpResults.Accepted`1", 202);
Add("Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute", 202);
Add("Microsoft.AspNetCore.Http.HttpResults.AcceptedAtRoute`1", 202);
Add("Microsoft.AspNetCore.Http.HttpResults.NoContent", 204);
Add("Microsoft.AspNetCore.Http.HttpResults.BadRequest", 400);
Add("Microsoft.AspNetCore.Http.HttpResults.BadRequest`1", 400);
Add("Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult", 401);
Add("Microsoft.AspNetCore.Http.HttpResults.NotFound", 404);
Add("Microsoft.AspNetCore.Http.HttpResults.NotFound`1", 404);
Add("Microsoft.AspNetCore.Http.HttpResults.Conflict", 409);
Add("Microsoft.AspNetCore.Http.HttpResults.Conflict`1", 409);
Add("Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity", 422);
Add("Microsoft.AspNetCore.Http.HttpResults.UnprocessableEntity`T", 422);
// Will be Supported in .NET 9
Add("Microsoft.AspNetCore.Http.HttpResults.InternalServerError", 500);
Add("Microsoft.AspNetCore.Http.HttpResults.InternalServerError`1", 500);
// Workleap's definition of the InternalServerError type result for other .NET versions
Add("Workleap.Extensions.OpenAPI.TypedResult.InternalServerError", 500);
Add("Workleap.Extensions.OpenAPI.TypedResult.InternalServerError`1", 500);

return dictionary;

void Add(string metadata, int statusCode)
foreach (var pair in HttpResultsStatusCodeTypeHelpers.HttpResultTypeToStatusCodes)
{
var type = compilation.GetTypeByMetadataName(metadata);
var type = compilation.GetTypeByMetadataName(pair.Key);
if (type is not null)
{
dictionary.Add(type, statusCode);
dictionary.Add(type, pair.Value);
}
}

return dictionary;
}

private static Dictionary<int, ITypeSymbol> InitializeStatusCodeMapHttpResultMap(Compilation compilation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\HttpResultsStatusCodeTypeHelpers.cs">
<Link>Shared\HttpResultsStatusCodeTypeHelpers.cs</Link>
</Compile>
</ItemGroup>

<ItemGroup>
<!--
"Microsoft.CodeAnalysis.*" packages allow the development of Roslyn analyzers and source generators.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ public static IEnumerable<object[]> GetData()
};
yield return new object[]
{
typeof(Results<Ok<TestTypedSchema>, BadRequest<ProblemDetails>, NotFound>), // TODO: Document from this example the output OpeanAPI document (also include Produce(json))
typeof(Accepted<TestTypedSchema>),
new List<ExtractSchemaTypeResultFilter.ResponseMetadata>
{
new((int)HttpStatusCode.Accepted, typeof(TestTypedSchema))
}
};
yield return new object[]
{
typeof(Results<Ok<TestTypedSchema>, Accepted<TestTypedSchema>, BadRequest<ProblemDetails>, NotFound>),
new List<ExtractSchemaTypeResultFilter.ResponseMetadata>
{
new((int)HttpStatusCode.OK, typeof(TestTypedSchema)),
new((int)HttpStatusCode.Accepted, typeof(TestTypedSchema)),
new((int)HttpStatusCode.BadRequest, typeof(ProblemDetails)),
new((int)HttpStatusCode.NotFound, null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,29 @@ internal static IEnumerable<ResponseMetadata> GetResponsesMetadata(Type returnTy
// Initialize an instance of the result type to get the response metadata and return null if it's not possible
private static ResponseMetadata? ExtractMetadataFromTypedResult(Type resultType)
{
if (ExtractStatusCodeFromType(resultType) is not { } statusCode)
{
return null;
}

// For type like Ok, BadRequest, NotFound
if (!resultType.GenericTypeArguments.Any())
{
var constructor = resultType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Array.Empty<Type>(), null);
if (constructor != null)
{
var instance = constructor.Invoke(Array.Empty<object>());
var statusCode = (instance as IStatusCodeHttpResult)?.StatusCode;

return new(statusCode ?? 0, null);
}
return new(statusCode, null);
}
// For types like Ok<T>, BadRequest<T>, NotFound<T>
else
{
var constructor = resultType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { resultType.GenericTypeArguments.First() }, null);
if (constructor != null)
{
var instance = constructor.Invoke(new object?[] { null });
var statusCode = (instance as IStatusCodeHttpResult)?.StatusCode;
return new(statusCode ?? 0, resultType.GenericTypeArguments.First());
}
return new(statusCode, resultType.GenericTypeArguments.First());
}
}

private static int? ExtractStatusCodeFromType(Type resultType)
{
var typeString = $"{resultType.Namespace}.{resultType.Name}";
if (HttpResultsStatusCodeTypeHelpers.HttpResultTypeToStatusCodes.TryGetValue(typeString, out var statusCode))
{
return statusCode;
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
<ProjectReference Include="..\Workleap.Extensions.OpenAPI.Analyzers\Workleap.Extensions.OpenAPI.Analyzers.csproj" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\HttpResultsStatusCodeTypeHelpers.cs">
<Link>Shared\HttpResultsStatusCodeTypeHelpers.cs</Link>
</Compile>
</ItemGroup>

<PropertyGroup>
<!--
Here we use some advanced MSBuild to embed our Roslyn analyzers into the generated package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ public Results<Ok<TypedResultExample>, BadRequest<ProblemDetails>, NotFound> Typ
};
}

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

[HttpGet]
[Route("/voidOk")]
public Ok VoidOk(int id)
Expand Down
27 changes: 27 additions & 0 deletions src/tests/WebApi.OpenAPI.SystemTest/openapi-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,33 @@ paths:
$ref: '#/components/schemas/ProblemDetails'
'404':
description: '404'
/withNoAnnotationForAcceptedAndUnprocessableResponse:
get:
tags:
- TypedResult
operationId: TypedResultWithNoAnnotationForAcceptedAndUnprocessableResponse
parameters:
- name: id
in: query
style: form
schema:
type: integer
format: int32
responses:
'200':
description: '200'
content:
application/json:
schema:
$ref: '#/components/schemas/TypedResultExample'
'202':
description: '202'
content:
application/json:
schema:
$ref: '#/components/schemas/TypedResultExample'
'422':
description: '422'
/voidOk:
get:
tags:
Expand Down
27 changes: 27 additions & 0 deletions src/tests/expected-openapi-document.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,33 @@ paths:
$ref: '#/components/schemas/ProblemDetails'
'404':
description: '404'
/withNoAnnotationForAcceptedAndUnprocessableResponse:
get:
tags:
- TypedResult
operationId: TypedResultWithNoAnnotationForAcceptedAndUnprocessableResponse
parameters:
- name: id
in: query
style: form
schema:
type: integer
format: int32
responses:
'200':
description: '200'
content:
application/json:
schema:
$ref: '#/components/schemas/TypedResultExample'
'202':
description: '202'
content:
application/json:
schema:
$ref: '#/components/schemas/TypedResultExample'
'422':
description: '422'
/voidOk:
get:
tags:
Expand Down

0 comments on commit 5398351

Please sign in to comment.