Skip to content

Commit 7f33097

Browse files
authored
Merge pull request #30 from Fresa/support-sequential-media-types
Support sequential media types
2 parents a3ff955 + 985fb9b commit 7f33097

25 files changed

Lines changed: 817 additions & 102 deletions

OpenAPI.WebApiGenerator.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
3434
EndProject
3535
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi", "tests\Example.OpenApi\Example.OpenApi.csproj", "{4E274740-E49C-4E56-9B69-C33D9409C119}"
3636
EndProject
37+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.WebApiGenerator.UnitTests", "tests\OpenAPI.WebApiGenerator.UnitTests\OpenAPI.WebApiGenerator.UnitTests.csproj", "{2CED6DCB-B934-438D-98F0-21C784B353EB}"
38+
EndProject
39+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
40+
EndProject
3741
Global
3842
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3943
Debug|Any CPU = Debug|Any CPU
@@ -188,6 +192,18 @@ Global
188192
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x64.Build.0 = Release|Any CPU
189193
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.ActiveCfg = Release|Any CPU
190194
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.Build.0 = Release|Any CPU
195+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
196+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
197+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x64.ActiveCfg = Debug|Any CPU
198+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x64.Build.0 = Debug|Any CPU
199+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x86.ActiveCfg = Debug|Any CPU
200+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Debug|x86.Build.0 = Debug|Any CPU
201+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
202+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|Any CPU.Build.0 = Release|Any CPU
203+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x64.ActiveCfg = Release|Any CPU
204+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x64.Build.0 = Release|Any CPU
205+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x86.ActiveCfg = Release|Any CPU
206+
{2CED6DCB-B934-438D-98F0-21C784B353EB}.Release|x86.Build.0 = Release|Any CPU
191207
EndGlobalSection
192208
GlobalSection(SolutionProperties) = preSolution
193209
HideSolutionNode = FALSE

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,26 @@ switch (request.TryMatchAcceptMediaType<Response.OK200>(out var matchedMediaType
167167
}
168168
```
169169

170+
## Sequential Media Types
171+
OpenAPI 3.2 added support for [sequential media types](https://spec.openapis.org/oas/v3.2.0.html#sequential-media-types). The following sequential media types are supported for both request and response media content:
172+
- application/jsonl
173+
- application/x-ndjson
174+
- application/x-jsonlines
175+
- application/json-seq
176+
- application/geo+json-seq
177+
178+
Other sequential media types can be implemented by simply following the expected naming convention and placing the implementations in the expected namespace, see the compilation error of any missing media type class.
179+
180+
### Request Content
181+
Inherit from `SequentialJsonEnumerable<T>` using the following naming convention:
182+
- application/jsonl (lower case) -> `ApplicationJsonlEnumerable<T>`
183+
184+
### Response Content
185+
Inherit from `SequentialJsonWriter<T>` using the following naming convention:
186+
- application/jsonl (lower case) -> `ApplicationJsonlWriter<T>`
187+
188+
See the [OpenAPI 3.2 examples](#examples) for further details how to consume and produce sequential media types.
189+
170190
## Authentication and Authorization
171191
OpenAPI defines [security scheme objects](https://spec.openapis.org/oas/latest#security-scheme-object) for authentication and authorization mechanisms. The generator implement endpoint filters that corresponds to the security declaration of each operation. Do _not_ call `UseAuthentication` or similar when configuring the application.
172192

src/OpenAPI.WebApiGenerator/ApiGenerator.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ private static void GenerateCode(SourceProductionContext context,
8989

9090
var validationExtensionsGenerator = new ValidationExtensionsGenerator(rootNamespace);
9191
validationExtensionsGenerator.GenerateClass().AddTo(context);
92+
var sequentialJsonEnumeratorsGenerator = new SequentialMediaTypesGenerator(rootNamespace);
93+
sequentialJsonEnumeratorsGenerator.GenerateClasses().AddTo(context);
9294

9395
var operations = new List<(string Namespace, KeyValuePair<HttpMethod, OpenApiOperation> Operation)>();
94-
var securityParameterGenerators = new ConcurrentDictionary<IOpenApiSecurityScheme, List<ParameterGenerator>>();
9596
foreach (var path in openApi.Paths)
9697
{
9798
var pathExpression = path.Key;
@@ -137,9 +138,10 @@ private static void GenerateCode(SourceProductionContext context,
137138
var schemaReference = openApiOperationVisitor.GetSchemaReference(mediaType);
138139
var typeDeclaration = schemaGenerator.Generate(schemaReference);
139140
return new RequestBodyContentGenerator(
140-
pair.Key,
141+
pair,
141142
typeDeclaration,
142-
httpRequestExtensionsGenerator);
143+
httpRequestExtensionsGenerator,
144+
sequentialJsonEnumeratorsGenerator);
143145
}).ToList();
144146
requestBodyGenerator = new RequestBodyGenerator(
145147
body,
@@ -164,14 +166,14 @@ private static void GenerateCode(SourceProductionContext context,
164166
var responseContent =
165167
// OpenAPI.NET is incorrectly adding content where there is none defined.
166168
// No content definition means NO content.
167-
response.Content?.Where(content =>
168-
openApiResponseVisitor.HasContent(content.Value)) ?? [];
169-
var responseBodyGenerators = responseContent.Select(valuePair =>
169+
response.Content?.Where(responseContent =>
170+
openApiResponseVisitor.HasContent(responseContent.Value)) ?? [];
171+
var responseBodyGenerators = responseContent.Select(mediaContent =>
170172
{
171-
var content = valuePair.Value;
172-
var contentSchemaReference = openApiResponseVisitor.GetSchemaReference(content);
173+
var contentMediaType = mediaContent.Value;
174+
var contentSchemaReference = openApiResponseVisitor.GetSchemaReference(contentMediaType);
173175
var typeDeclaration = schemaGenerator.Generate(contentSchemaReference);
174-
return new ResponseBodyContentGenerator(valuePair.Key, typeDeclaration);
176+
return new ResponseBodyContentGenerator(mediaContent, typeDeclaration);
175177
}).ToList();
176178

177179
var responseHeaderGenerators = response.Headers?.Select(valuePair =>

src/OpenAPI.WebApiGenerator/CodeGeneration/AuthGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ internal abstract class BaseSecurityRequirementsFilter(WebApiConfiguration confi
262262
break;
263263
}
264264
265-
authorized &= ClaimContainsScopes(authenticateResult.Principal, configuration.SecuritySchemeOptions.GetScopeOptions(scheme), scopes);
265+
authorized &= ClaimContainsScopes(authenticateResult.Principal, Configuration.SecuritySchemeOptions.GetScopeOptions(scheme), scopes);
266266
if (!authorized)
267267
break;
268268
}

src/OpenAPI.WebApiGenerator/CodeGeneration/OperationGenerator.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ internal partial class Operation
6868
private Func<Request, ImmutableList<ValidationResult>, Response> HandleRequestValidationError { get; } = (_, validationResult) =>
6969
{{jsonValidationExceptionGenerator.CreateThrowJsonValidationExceptionInvocation("Request is not valid", "validationResult")}};
7070
71+
/// <summary>
72+
/// Create a request validation error response.
73+
/// <exception cref="JsonValidationException"></exception>
74+
/// <param name="request">The invalid request</param>
75+
/// <param name="ValidationContext">The validation context describing the validation errors</param>
76+
/// </summary>
77+
private Response CreateRequestValidationErrorResponse(Request request, ValidationContext validationContext)
78+
{
79+
var configuration = request.HttpContext.RequestServices.GetRequiredService<WebApiConfiguration>();
80+
return HandleRequestValidationError(request, validationContext.Results.WithLocation(configuration.OpenApiSpecificationUri));
81+
}
82+
7183
{{authGenerator.GenerateAuthFilters(operation.Operation, parameters, out var requiresAuth).Indent(4)}}
7284
{{(requiresAuth ?
7385
"""
Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,51 @@
1-
using System.Net.Http.Headers;
1+
using System.Collections.Generic;
2+
using System.Net.Http.Headers;
23
using Corvus.Json.CodeGeneration;
34
using Corvus.Json.CodeGeneration.CSharp;
5+
using Microsoft.OpenApi;
46
using OpenAPI.WebApiGenerator.Extensions;
57

68
namespace OpenAPI.WebApiGenerator.CodeGeneration;
79

810
internal sealed class RequestBodyContentGenerator(
9-
string contentType,
11+
KeyValuePair<string, IOpenApiMediaType> contentMediaType,
1012
TypeDeclaration typeDeclaration,
11-
HttpRequestExtensionsGenerator httpRequestExtensionsGenerator)
13+
HttpRequestExtensionsGenerator httpRequestExtensionsGenerator,
14+
SequentialMediaTypesGenerator sequentialMediaTypesGenerator)
1215
{
13-
private string FullyQualifiedTypeName =>
14-
$"{FullyQualifiedTypeDeclarationIdentifier}?";
15-
1616
private string FullyQualifiedTypeDeclarationIdentifier => typeDeclaration.FullyQualifiedDotnetTypeName();
17-
18-
internal string PropertyName { get; } = contentType.ToPascalCase();
19-
20-
internal MediaTypeWithQualityHeaderValue ContentType { get; } = MediaTypeWithQualityHeaderValue.Parse(contentType);
17+
private readonly bool _isSequentialMediaType = contentMediaType.Value.ItemSchema != null;
18+
19+
internal string PropertyName { get; } = contentMediaType.Key.ToPascalCase();
20+
internal bool IsPropertyStruct => !_isSequentialMediaType;
21+
22+
internal MediaTypeWithQualityHeaderValue ContentType { get; } = MediaTypeWithQualityHeaderValue.Parse(contentMediaType.Key);
2123

2224
internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
2325
internal string GenerateRequestBindingDirective() =>
2426
$"""
25-
{PropertyName} =
26-
({httpRequestExtensionsGenerator.CreateBindBodyInvocation(
27-
"request",
28-
FullyQualifiedTypeDeclarationIdentifier)
29-
.Indent(8).Trim()})
27+
{PropertyName} = {(_isSequentialMediaType ?
28+
$"{sequentialMediaTypesGenerator.GenerateConstructorInstance(
29+
ContentType,
30+
typeDeclaration,
31+
"request.Body")}" :
32+
$"({httpRequestExtensionsGenerator.CreateBindBodyInvocation(
33+
"request",
34+
FullyQualifiedTypeDeclarationIdentifier).Indent(8).Trim()})")}
3035
""";
36+
3137

32-
public string GenerateRequestProperty() =>
33-
$$"""
34-
/// <summary>
35-
/// Request content for {{contentType}}
36-
/// </summary>
37-
internal {{FullyQualifiedTypeName}} {{PropertyName}} { get; private set; }
38-
""";
38+
public string GenerateRequestProperty()
39+
{
40+
var fullyQualifiedTypeName = _isSequentialMediaType
41+
? sequentialMediaTypesGenerator.GetFullyQualifiedTypeName(ContentType, typeDeclaration)
42+
: FullyQualifiedTypeDeclarationIdentifier;
43+
return
44+
$$"""
45+
/// <summary>
46+
/// Request content for {{contentMediaType.Key}}
47+
/// </summary>
48+
internal {{fullyQualifiedTypeName}}? {{PropertyName}} { get; private set; }
49+
""";
50+
}
3951
}

src/OpenAPI.WebApiGenerator/CodeGeneration/RequestBodyGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ internal ValidationContext Validate(ValidationContext validationContext, Validat
120120
{{{_contentGenerators.AggregateToString(content =>
121121
$"""
122122
true when {content.PropertyName} is not null =>
123-
{content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
123+
{content.PropertyName}!.{(content.IsPropertyStruct ? "Value." : "")}Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
124124
""")}}
125125
true when requestContentType is null =>
126126
{{(_body.Required ? """validationContext.WithResult(false, "Request content is missing")""" : "validationContext")}},

0 commit comments

Comments
 (0)