Skip to content

Commit f52280b

Browse files
authored
Merge pull request #28 from Fresa/handle-media-type-range
Handle media type range
2 parents c8fa5af + 368f79e commit f52280b

11 files changed

Lines changed: 348 additions & 39 deletions

File tree

src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyContentGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Corvus.Json.CodeGeneration;
1+
using System.Net.Http.Headers;
2+
using Corvus.Json.CodeGeneration;
23
using Corvus.Json.CodeGeneration.CSharp;
34
using OpenAPI.WebApiGenerator.Extensions;
45

@@ -16,7 +17,7 @@ internal sealed class RequestBodyContentGenerator(
1617

1718
internal string PropertyName { get; } = contentType.ToPascalCase();
1819

19-
internal string ContentType => contentType;
20+
internal MediaTypeHeaderValue ContentType { get; } = MediaTypeHeaderValue.Parse(contentType);
2021

2122
internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
2223
internal string GenerateRequestBindingDirective() =>
@@ -26,7 +27,6 @@ internal string GenerateRequestBindingDirective() =>
2627
"request",
2728
FullyQualifiedTypeDeclarationIdentifier)
2829
.Indent(8).Trim()})
29-
.AsOptional()
3030
""";
3131

3232
public string GenerateRequestProperty() =>

src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using Microsoft.OpenApi;
45
using OpenAPI.WebApiGenerator.Extensions;
56

@@ -23,7 +24,10 @@ public RequestBodyGenerator(
2324
List<RequestBodyContentGenerator> contentGenerators)
2425
{
2526
_body = body;
26-
_contentGenerators = contentGenerators;
27+
_contentGenerators = contentGenerators
28+
.OrderByDescending(generator =>
29+
generator.ContentType.GetPrecedence())
30+
.ToList();
2731
}
2832

2933
internal static readonly RequestBodyGenerator Empty = new();
@@ -71,7 +75,7 @@ public string GenerateRequestProperty(string propertyName)
7175
/// <summary>
7276
/// Request content
7377
/// </summary>
74-
internal sealed class RequestContent
78+
internal sealed class RequestContent(string? requestContentType, bool invalidContentType = false)
7579
{{{
7680
_contentGenerators.AggregateToString(content =>
7781
content.GenerateRequestProperty()).Indent(4)}}
@@ -88,21 +92,21 @@ internal sealed class RequestContent
8892
var requestContentType = request.ContentType;
8993
var requestContentMediaType = requestContentType == null ? null : System.Net.Http.Headers.MediaTypeHeaderValue.Parse(requestContentType);
9094
91-
switch (requestContentMediaType?.MediaType?.ToLower())
95+
switch (requestContentMediaType?.MediaType)
9296
{{{_contentGenerators.AggregateToString(content =>
9397
$$"""
94-
case "{{content.ContentType.ToLower()}}":
95-
return new RequestContent
98+
case not null when {{content.ContentType.GetMatchConditionExpression("requestContentMediaType")}}:
99+
return new RequestContent(requestContentType)
96100
{
97101
{{content.GenerateRequestBindingDirective().Indent(20)}}
98102
};
99103
""")}}{{(_body.Required ? "" :
100104
"""
101-
case "":
105+
case null:
102106
return null;
103107
""")}}
104108
default:
105-
throw new BadHttpRequestException($"Request body does not support content type {requestContentType}");
109+
return new RequestContent(requestContentType, true);
106110
}
107111
}
108112
@@ -112,22 +116,19 @@ internal sealed class RequestContent
112116
/// <param name="validationContext">Current validation context</param>
113117
/// <param name="validationLevel">Validation level</param>
114118
/// <returns>The validation result</returns>
115-
internal ValidationContext Validate(ValidationContext validationContext, ValidationLevel validationLevel)
116-
{
117-
switch (true)
119+
internal ValidationContext Validate(ValidationContext validationContext, ValidationLevel validationLevel) =>
120+
true switch
118121
{{{_contentGenerators.AggregateToString(content =>
119122
$"""
120-
case true when {content.PropertyName} is not null:
121-
return {content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel);
122-
""")}}
123-
default:
124-
{{(_body.Required ?
125-
"""
126-
throw new InvalidOperationException("Request body not set");
127-
""" :
128-
"return validationContext;")}}
129-
}
130-
}
123+
true when {content.PropertyName} is not null =>
124+
{content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
125+
""")}}
126+
true when requestContentType is null =>
127+
{{(_body.Required ? """validationContext.WithResult(false, "Request content is missing")""" : "validationContext")}},
128+
true when invalidContentType =>
129+
validationContext.WithResult(false, $"Request content type {requestContentType} is not supported"),
130+
_ => validationContext
131+
};
131132
}
132133
""";
133134
}
Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,75 @@
1-
using Corvus.Json.CodeGeneration;
1+
using System;
2+
using System.Net.Http.Headers;
3+
using Corvus.Json.CodeGeneration;
24
using Corvus.Json.CodeGeneration.CSharp;
35
using OpenAPI.WebApiGenerator.Extensions;
46

57
namespace OpenAPI.WebApiGenerator.CodeGeneration;
68

7-
internal sealed class ResponseBodyContentGenerator(string contentType, TypeDeclaration typeDeclaration)
9+
internal sealed class ResponseBodyContentGenerator
810
{
9-
private readonly string _contentVariableName = contentType.ToCamelCase();
10-
public string ContentPropertyName { get; } = contentType.ToPascalCase();
11-
internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
11+
private readonly string _contentVariableName;
12+
public string ContentPropertyName { get; }
13+
private readonly MediaTypeHeaderValue _contentType;
14+
private readonly TypeDeclaration _typeDeclaration;
15+
private readonly bool _isContentTypeRange;
16+
17+
public ResponseBodyContentGenerator(string contentType, TypeDeclaration typeDeclaration)
18+
{
19+
_contentType = MediaTypeHeaderValue.Parse(contentType);
20+
_typeDeclaration = typeDeclaration;
21+
ContentPropertyName = contentType.ToPascalCase();
22+
23+
_isContentTypeRange = false;
24+
switch (_contentType.MediaType)
25+
{
26+
case "*/*":
27+
_contentVariableName = "any";
28+
_isContentTypeRange = true;
29+
break;
30+
case not null when _contentType.MediaType.EndsWith("*"):
31+
_contentVariableName = $"any{_contentType.MediaType.TrimEnd('*').TrimEnd('/').ToPascalCase()}";
32+
_isContentTypeRange = true;
33+
break;
34+
case null:
35+
throw new InvalidOperationException("Content type is null");
36+
default:
37+
_contentVariableName = _contentType.MediaType.ToCamelCase();
38+
break;
39+
}
40+
41+
ContentPropertyName = _contentVariableName.ToPascalCase();
42+
}
43+
44+
internal string SchemaLocation => _typeDeclaration.RelativeSchemaLocation;
1245
public string GenerateConstructor(string className, string contentTypeFieldName) =>
1346
$$"""
1447
/// <summary>
15-
/// Construct content for {{contentType}}
48+
/// Construct content for {{_contentType}}
1649
/// </summary>
17-
/// <param name="{{_contentVariableName}}">Content</param>
18-
public {{className}}({{typeDeclaration.FullyQualifiedDotnetTypeName()}} {{_contentVariableName}})
19-
{
50+
/// <param name="{{_contentVariableName}}">Content</param>{{(_isContentTypeRange ? $"""
51+
52+
/// <param name="contentType">Content type must match range {_contentType.MediaType}</param>
53+
""" : "")}}
54+
public {{className}}({{_typeDeclaration.FullyQualifiedDotnetTypeName()}} {{_contentVariableName}}{{(_isContentTypeRange ? ", string contentType" : "")}})
55+
{{{(_isContentTypeRange ?
56+
$$"""
57+
58+
EnsureExpectedContentType(MediaTypeHeaderValue.Parse(contentType), MediaTypeHeaderValue.Parse("{{_contentType}}"));
59+
""" : "")}}
2060
{{ContentPropertyName}} = {{_contentVariableName}};
21-
{{contentTypeFieldName}} = "{{contentType}}";
22-
}
61+
{{contentTypeFieldName}} = {{(_isContentTypeRange ? "contentType" : $"\"{_contentType.MediaType}\"")}};
62+
}
2363
""";
2464

2565
public string GenerateContentProperty()
2666
{
2767
return
2868
$$"""
2969
/// <summary>
30-
/// Content for {{contentType}}
70+
/// Content for {{_contentType}}
3171
/// </summary>
32-
internal {{typeDeclaration.FullyQualifiedDotnetTypeName()}}? {{ContentPropertyName}} { get; }
72+
internal {{_typeDeclaration.FullyQualifiedDotnetTypeName()}}? {{ContentPropertyName}} { get; }
3373
""";
3474
}
3575
}

src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public SourceCode GenerateResponseClass(string @namespace, string path)
1414
$$"""
1515
#nullable enable
1616
using Corvus.Json;
17+
using System.Net.Http.Headers;
1718
using System.Text.Json;
1819
using {{httpResponseExtensionsGenerator.Namespace}};
1920
@@ -35,6 +36,28 @@ internal abstract partial class Response
3536
=> (code >= {{i}}00 && code <= {{i}}99) ? code : throw new InvalidOperationException($"Expected {{i}}xx status code, got {code}");
3637
""")}}
3738
39+
/// <summary>
40+
/// Ensures that the specified content type matches the specification
41+
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified content type does not match the specification</exception>
42+
/// </summary>
43+
/// <param name="contentType">Content type</param>
44+
/// <param name="expectedContentType">Expected content type</param>
45+
protected void EnsureExpectedContentType(MediaTypeHeaderValue contentType, MediaTypeHeaderValue expectedContentType)
46+
{
47+
var valid = expectedContentType.MediaType switch
48+
{
49+
"*/*" => true,
50+
not null when expectedContentType.MediaType.EndsWith("*") =>
51+
contentType.MediaType?.StartsWith(expectedContentType.MediaType.TrimEnd('*'), StringComparison.OrdinalIgnoreCase) ?? false,
52+
not null => contentType.MediaType?.Equals(expectedContentType.MediaType, StringComparison.OrdinalIgnoreCase) ?? false,
53+
_ => false
54+
};
55+
56+
if (valid)
57+
return;
58+
throw new ArgumentOutOfRangeException($"Expected content type {contentType.MediaType} to match range {expectedContentType.MediaType}");
59+
}
60+
3861
/// <summary>
3962
/// Write the response to a http response object
4063
/// </summary>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Net.Http.Headers;
4+
5+
namespace OpenAPI.WebApiGenerator.Extensions;
6+
7+
internal static class MediaTypeExtensions
8+
{
9+
internal static string GetMatchConditionExpression(this MediaTypeHeaderValue value, string mediaTypeVariableName)
10+
{
11+
var expressions = new List<string>();
12+
if (value.MediaType is not null)
13+
{
14+
expressions.Add(value.MediaType switch
15+
{
16+
"*/*" => "true",
17+
not null when value.MediaType.EndsWith("*") =>
18+
$"""{mediaTypeVariableName}.{nameof(value.MediaType)}.{nameof(value.MediaType.StartsWith)}("{value.MediaType.TrimEnd('*')}", StringComparison.OrdinalIgnoreCase)""",
19+
_ =>
20+
$"""{mediaTypeVariableName}.{nameof(value.MediaType)}.{nameof(value.MediaType.Equals)}("{value.MediaType}", StringComparison.OrdinalIgnoreCase)"""
21+
});
22+
}
23+
24+
expressions.AddRange(value.Parameters.Select(parameter =>
25+
$"{mediaTypeVariableName}.{nameof(value.Parameters)}.{nameof(value.Parameters.Contains)}({(parameter.Value is null ?
26+
$"""new NameValueHeaderValue("{parameter.Name}")""" :
27+
$"""new NameValueHeaderValue("{parameter.Name}", "{parameter.Value}")""")})"));
28+
29+
return string.Join(" && ", expressions);
30+
}
31+
32+
internal static int GetPrecedence(this MediaTypeHeaderValue value) =>
33+
value.Parameters.Count + value.MediaType switch
34+
{
35+
"*/*" => 0,
36+
not null when value.MediaType.EndsWith("*") => 100,
37+
_ => 1000
38+
};
39+
}

src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,7 @@
8282
</ItemGroup>
8383
</Target>
8484

85+
<ItemGroup>
86+
<InternalsVisibleTo Include="OpenAPI.WebApiGenerator.Tests" />
87+
</ItemGroup>
8588
</Project>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
2727
_ = request.Header.Bar;
2828

2929
var response = new Response.OK200(Components.Schemas.FooProperties.Create(
30-
name: request.Body.ApplicationJson?.Name))
30+
name: request.Body.ApplicationJson?.Name), "application/json")
3131
{
3232
Headers = new Response.OK200.ResponseHeaders
3333
{

tests/Example.OpenApi32/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
}
4949
},
5050
"content": {
51-
"application/json": {
51+
"application/*": {
5252
"schema": {
5353
"$ref": "#/components/schemas/FooProperties"
5454
}

0 commit comments

Comments
 (0)