Skip to content

Commit a7ddff6

Browse files
committed
refactor(response): make response content comparison type safe
1 parent ec1a83c commit a7ddff6

9 files changed

Lines changed: 72 additions & 69 deletions

File tree

src/OpenAPI.WebApiGenerator/CodeGeneration/HttpRequestExtensionsGenerator.cs

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -186,49 +186,6 @@ private static T Parse<T>(IParameterValueParser parser, string? value)
186186
187187
return instance == null ? T.Null : T.Parse(instance.ToJsonString());
188188
}
189-
190-
/// <summary>
191-
/// Returns the best match if an acceptable media type is found.
192-
/// </summary>
193-
/// <param name="mediaTypes">Media types to match against</param>
194-
/// <param name="matchedMediaType">Matched media type if method returns true</param>
195-
/// <returns>True if a matched media type was found</returns>
196-
internal static bool TryMatchAcceptMediaType(
197-
this HttpRequest request,
198-
MediaTypeHeaderValue[] mediaTypes,
199-
[NotNullWhen(true)] out MediaTypeHeaderValue? matchedMediaType)
200-
{
201-
var acceptHeaders = request.GetTypedHeaders().Accept;
202-
if (acceptHeaders is not { Count: > 0 })
203-
{
204-
matchedMediaType = mediaTypes.Length > 0 ? mediaTypes[0] : null;
205-
return matchedMediaType != null;
206-
}
207-
208-
var sortedAcceptMediaTypes = acceptHeaders
209-
.OrderByDescending(headerValue => headerValue.Quality ?? 1.0)
210-
.ThenByDescending(headerValue => headerValue.MatchesAllTypes ? 0 : headerValue.MatchesAllSubTypes ? 1 : 2)
211-
.ThenByDescending(headerValue => headerValue.Parameters.Count);
212-
213-
foreach (var acceptMediaType in sortedAcceptMediaTypes)
214-
{
215-
if ((acceptMediaType.Quality ?? 1.0) <= 0)
216-
continue;
217-
218-
foreach (var mediaType in mediaTypes)
219-
{
220-
if (!mediaType.IsSubsetOf(acceptMediaType))
221-
{
222-
continue;
223-
}
224-
matchedMediaType = mediaType;
225-
return true;
226-
}
227-
}
228-
229-
matchedMediaType = null;
230-
return false;
231-
}
232189
}
233190
#nullable restore
234191
"""");

src/OpenAPI.WebApiGenerator/CodeGeneration/RequestGenerator.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ internal SourceCode GenerateRequestClass(string @namespace, string path)
3131
$$"""
3232
#nullable enable
3333
using Corvus.Json;
34-
using System.Diagnostics.CodeAnalysis;
35-
using Microsoft.Net.Http.Headers;
3634
3735
namespace {{@namespace}};
3836
@@ -73,16 +71,6 @@ internal partial class Request
7371
return {{(isAsync ? "request" : "Task.FromResult(request)")}};
7472
}
7573
76-
/// <summary>
77-
/// Returns the best match if an acceptable media type is found.
78-
/// </summary>
79-
/// <param name="mediaTypes">Media types to match against</param>
80-
/// <param name="matchedMediaType">Matched media type if method returns true</param>
81-
/// <returns>True if a matched media type was found</returns>
82-
internal bool TryMatchAcceptMediaType<T>(
83-
[NotNullWhen(true)] out MediaTypeHeaderValue? matchedMediaType) where T : class, IResponse =>
84-
HttpContext.Request.TryMatchAcceptMediaType(T.ContentMediaTypes, out matchedMediaType);
85-
8674
/// <summary>
8775
/// Validate the request
8876
/// </summary>

src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseBodyContentGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ internal sealed class {{ClassName}} : {{responseClassName}}
6666
{{contentTypeFieldName}} = {{(_isContentTypeRange ? "contentType" : $"\"{_contentType.MediaType}\"")}};
6767
}
6868
69-
internal static readonly MediaTypeHeaderValue ContentMediaType = MediaTypeHeaderValue.Parse("{{_contentType}}");
69+
/// <inheritdoc/>
70+
public static ContentMediaType<{{responseClassName}}> ContentMediaType { get; } = new(MediaTypeHeaderValue.Parse("{{_contentType}}"));
7071
protected override IJsonValue Content { get; }
7172
protected override string ContentSchemaLocation { get; } = "{{SchemaLocation}}";
7273
}

src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseContentGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string GenerateResponseContentClass()
6262
return
6363
$$$"""
6464
{{{_response.Description.AsComment("summary", "para")}}}
65-
internal {{{(_contentGenerators.Any() ? "abstract" : "sealed")}}} class {{{_responseClassName}}} : Response{{{(_contentGenerators.Any() ? ", IResponse" : "")}}}
65+
internal {{{(_contentGenerators.Any() ? "abstract" : "sealed")}}} class {{{_responseClassName}}} : Response{{{(_contentGenerators.Any() ? $", IResponse<{_responseClassName}>" : "")}}}
6666
{
6767
private string? {{{contentTypeFieldName}}} = null;{{{
6868
_contentGenerators.AggregateToString(generator =>
@@ -75,7 +75,7 @@ public string GenerateResponseContentClass()
7575
protected abstract string ContentSchemaLocation { get; }
7676
7777
/// <inheritdoc/>
78-
public static MediaTypeHeaderValue[] ContentMediaTypes { get; } =
78+
public static ContentMediaType<{{_responseClassName}}>[] ContentMediaTypes { get; } =
7979
[{{_contentGenerators.AggregateToString(generator =>
8080
$$"""
8181
{{generator.ClassName}}.ContentMediaType,

src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public SourceCode GenerateResponseClass(string @namespace, string path)
1515
#nullable enable
1616
using Corvus.Json;
1717
using Microsoft.Net.Http.Headers;
18+
using System.Diagnostics.CodeAnalysis;
1819
using System.Text.Json;
1920
using {{httpResponseExtensionsGenerator.Namespace}};
2021
@@ -68,12 +69,68 @@ protected void EnsureExpectedContentType(MediaTypeHeaderValue contentType, Media
6869
}}
6970
}
7071
71-
internal interface IResponse
72+
internal interface IResponse<T> where T : class
7273
{
7374
/// <summary>
74-
/// Content media types
75+
/// Contents for this response
7576
/// </summary>
76-
internal static abstract MediaTypeHeaderValue[] ContentMediaTypes { get; }
77+
internal static abstract ContentMediaType<T>[] ContentMediaTypes { get; }
78+
}
79+
80+
internal interface IContent<T> where T : class, IResponse<T>
81+
{
82+
internal static abstract ContentMediaType<T> ContentMediaType { get; }
83+
}
84+
85+
internal readonly record struct ContentMediaType<T>(MediaTypeHeaderValue Value)
86+
where T : class
87+
{
88+
public static implicit operator MediaTypeHeaderValue(ContentMediaType<T> mediaType) => mediaType.Value;
89+
}
90+
91+
internal partial class Request
92+
{
93+
/// <summary>
94+
/// Returns the best response content media type match if an acceptable media type is found.
95+
/// </summary>
96+
/// <param name="matchedContentMediaType">Matched content media type if method returns true</param>
97+
/// <typeparam name="T">The response to match against</typeparam>
98+
/// <returns>True if a matched media type was found</returns>
99+
internal bool TryMatchAcceptMediaType<T>(
100+
[NotNullWhen(true)] out ContentMediaType<T>? matchedContentMediaType) where T : class, IResponse<T>
101+
{
102+
var mediaTypes = T.ContentMediaTypes;
103+
var acceptHeaders = HttpContext.Request.GetTypedHeaders().Accept;
104+
if (acceptHeaders is not { Count: > 0 })
105+
{
106+
matchedContentMediaType = mediaTypes.Length > 0 ? mediaTypes[0] : null;
107+
return matchedContentMediaType != null;
108+
}
109+
110+
var sortedAcceptMediaTypes = acceptHeaders
111+
.OrderByDescending(headerValue => headerValue.Quality ?? 1.0)
112+
.ThenByDescending(headerValue => headerValue.MatchesAllTypes ? 0 : headerValue.MatchesAllSubTypes ? 1 : 2)
113+
.ThenByDescending(headerValue => headerValue.Parameters.Count);
114+
115+
foreach (var acceptMediaType in sortedAcceptMediaTypes)
116+
{
117+
if ((acceptMediaType.Quality ?? 1.0) <= 0)
118+
continue;
119+
120+
foreach (var mediaType in mediaTypes)
121+
{
122+
if (!mediaType.Value.IsSubsetOf(acceptMediaType))
123+
{
124+
continue;
125+
}
126+
matchedContentMediaType = mediaType;
127+
return true;
128+
}
129+
}
130+
131+
matchedContentMediaType = null;
132+
return false;
133+
}
77134
}
78135
#nullable restore
79136
""");

tests/Example.OpenApi20/Paths/FooFooId/Put/Operation.Handler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ private static Response.BadRequest400 HandleValidationErrors(Request request, Im
1717
switch (request.TryMatchAcceptMediaType<Response.BadRequest400>(out var matchedMediaType))
1818
{
1919
case false:
20-
case true when ReferenceEquals(matchedMediaType, Response.BadRequest400.ApplicationJson.ContentMediaType):
20+
case true when matchedMediaType == Response.BadRequest400.ApplicationJson.ContentMediaType:
2121
var response = validationResults.Select(result =>
2222
Responses.BadRequest.RequiredErrorAndName.Create(
2323
name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
@@ -38,7 +38,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
3838
switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType))
3939
{
4040
case false:
41-
case true when ReferenceEquals(matchedMediaType, Response.OK200.ApplicationJson.ContentMediaType):
41+
case true when matchedMediaType == Response.OK200.ApplicationJson.ContentMediaType:
4242
var response = new Response.OK200.ApplicationJson(Definitions.FooProperties.Create(
4343
name: request.Body.ApplicationJson?.Name))
4444
{

tests/Example.OpenApi30/Paths/FooFooId/Put/Operation.Handler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ private static Response.BadRequest400 HandleValidationErrors(Request request, Im
1616
switch (request.TryMatchAcceptMediaType<Response.BadRequest400>(out var matchedMediaType))
1717
{
1818
case false:
19-
case true when ReferenceEquals(matchedMediaType, Response.BadRequest400.ApplicationJson.ContentMediaType):
19+
case true when matchedMediaType == Response.BadRequest400.ApplicationJson.ContentMediaType:
2020
var response = validationResults.Select(result =>
2121
Components.Responses.BadRequest.Content.ApplicationJson.RequiredErrorAndName.Create(
2222
name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
@@ -37,7 +37,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
3737
switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType))
3838
{
3939
case false:
40-
case true when ReferenceEquals(matchedMediaType, Response.OK200.ApplicationJson.ContentMediaType):
40+
case true when matchedMediaType == Response.OK200.ApplicationJson.ContentMediaType:
4141
return Task.FromResult<Response>(new Response.OK200.ApplicationJson(
4242
Components.Schemas.FooProperties.Create(name: request.Body.ApplicationJson?.Name))
4343
{

tests/Example.OpenApi31/Paths/FooFooId/Put/Operation.Handler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private static Response.BadRequest400 HandleValidationErrors(Request request, Im
1515
switch (request.TryMatchAcceptMediaType<Response.BadRequest400>(out var matchedMediaType))
1616
{
1717
case false:
18-
case true when ReferenceEquals(matchedMediaType, Response.BadRequest400.ApplicationJson.ContentMediaType):
18+
case true when matchedMediaType == Response.BadRequest400.ApplicationJson.ContentMediaType:
1919
var response = validationResults.Select(result =>
2020
Components.Responses.BadRequest.Content.ApplicationJson.RequiredErrorAndName.Create(
2121
name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
@@ -36,7 +36,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
3636
switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType))
3737
{
3838
case false:
39-
case true when ReferenceEquals(matchedMediaType, Response.OK200.ApplicationJson.ContentMediaType):
39+
case true when matchedMediaType == Response.OK200.ApplicationJson.ContentMediaType:
4040
return Task.FromResult<Response>(new Response.OK200.ApplicationJson(
4141
Components.Schemas.FooProperties.Create(name: request.Body.ApplicationJson?.Name))
4242
{

tests/Example.OpenApi32/Paths/FooFooId/Put/Operation.Handler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private static Response.BadRequest400 HandleValidationErrors(Request request, Im
1515
switch (request.TryMatchAcceptMediaType<Response.BadRequest400>(out var matchedMediaType))
1616
{
1717
case false:
18-
case true when ReferenceEquals(matchedMediaType, Response.BadRequest400.ApplicationJson.ContentMediaType):
18+
case true when matchedMediaType == Response.BadRequest400.ApplicationJson.ContentMediaType:
1919
var response = validationResults.Select(result =>
2020
Components.Responses.BadRequest.Content.ApplicationJson.RequiredErrorAndName.Create(
2121
name: result.Location?.SchemaLocation.ToString() ?? string.Empty,
@@ -35,7 +35,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
3535
switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType))
3636
{
3737
case false:
38-
case true when ReferenceEquals(matchedMediaType, Response.OK200.AnyApplication.ContentMediaType):
38+
case true when matchedMediaType == Response.OK200.AnyApplication.ContentMediaType:
3939
return Task.FromResult<Response>(new Response.OK200.AnyApplication(
4040
Components.Schemas.FooProperties.Create(name: request.Body.ApplicationJson?.Name),
4141
"application/json") { Headers = new Response.OK200.ResponseHeaders { Status = 2 } });

0 commit comments

Comments
 (0)