Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Corvus.Json.CodeGeneration;
using System.Net.Http.Headers;
using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using OpenAPI.WebApiGenerator.Extensions;

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

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

internal string ContentType => contentType;
internal MediaTypeHeaderValue ContentType { get; } = MediaTypeHeaderValue.Parse(contentType);

internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
internal string GenerateRequestBindingDirective() =>
Expand All @@ -26,7 +27,6 @@ internal string GenerateRequestBindingDirective() =>
"request",
FullyQualifiedTypeDeclarationIdentifier)
.Indent(8).Trim()})
.AsOptional()
""";

public string GenerateRequestProperty() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.Extensions;

Expand All @@ -23,7 +24,10 @@ public RequestBodyGenerator(
List<RequestBodyContentGenerator> contentGenerators)
{
_body = body;
_contentGenerators = contentGenerators;
_contentGenerators = contentGenerators
.OrderByDescending(generator =>
generator.ContentType.GetPrecedence())
.ToList();
}

internal static readonly RequestBodyGenerator Empty = new();
Expand Down Expand Up @@ -71,7 +75,7 @@ public string GenerateRequestProperty(string propertyName)
/// <summary>
/// Request content
/// </summary>
internal sealed class RequestContent
internal sealed class RequestContent(string? requestContentType, bool invalidContentType = false)
{{{
_contentGenerators.AggregateToString(content =>
content.GenerateRequestProperty()).Indent(4)}}
Expand All @@ -88,21 +92,21 @@ internal sealed class RequestContent
var requestContentType = request.ContentType;
var requestContentMediaType = requestContentType == null ? null : System.Net.Http.Headers.MediaTypeHeaderValue.Parse(requestContentType);

switch (requestContentMediaType?.MediaType?.ToLower())
switch (requestContentMediaType?.MediaType)
{{{_contentGenerators.AggregateToString(content =>
$$"""
case "{{content.ContentType.ToLower()}}":
return new RequestContent
case not null when {{content.ContentType.GetMatchConditionExpression("requestContentMediaType")}}:
return new RequestContent(requestContentType)
{
{{content.GenerateRequestBindingDirective().Indent(20)}}
};
""")}}{{(_body.Required ? "" :
"""
case "":
case null:
return null;
""")}}
default:
throw new BadHttpRequestException($"Request body does not support content type {requestContentType}");
return new RequestContent(requestContentType, true);
}
}

Expand All @@ -112,22 +116,19 @@ internal sealed class RequestContent
/// <param name="validationContext">Current validation context</param>
/// <param name="validationLevel">Validation level</param>
/// <returns>The validation result</returns>
internal ValidationContext Validate(ValidationContext validationContext, ValidationLevel validationLevel)
{
switch (true)
internal ValidationContext Validate(ValidationContext validationContext, ValidationLevel validationLevel) =>
true switch
{{{_contentGenerators.AggregateToString(content =>
$"""
case true when {content.PropertyName} is not null:
return {content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel);
""")}}
default:
{{(_body.Required ?
"""
throw new InvalidOperationException("Request body not set");
""" :
"return validationContext;")}}
}
}
true when {content.PropertyName} is not null =>
{content.PropertyName}!.Value.Validate("{content.SchemaLocation}", true, validationContext, validationLevel),
""")}}
true when requestContentType is null =>
{{(_body.Required ? """validationContext.WithResult(false, "Request content is missing")""" : "validationContext")}},
true when invalidContentType =>
validationContext.WithResult(false, $"Request content type {requestContentType} is not supported"),
_ => validationContext
};
}
""";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,75 @@
using Corvus.Json.CodeGeneration;
using System;
using System.Net.Http.Headers;
using Corvus.Json.CodeGeneration;
using Corvus.Json.CodeGeneration.CSharp;
using OpenAPI.WebApiGenerator.Extensions;

namespace OpenAPI.WebApiGenerator.CodeGeneration;

internal sealed class ResponseBodyContentGenerator(string contentType, TypeDeclaration typeDeclaration)
internal sealed class ResponseBodyContentGenerator
{
private readonly string _contentVariableName = contentType.ToCamelCase();
public string ContentPropertyName { get; } = contentType.ToPascalCase();
internal string SchemaLocation => typeDeclaration.RelativeSchemaLocation;
private readonly string _contentVariableName;
public string ContentPropertyName { get; }
private readonly MediaTypeHeaderValue _contentType;
private readonly TypeDeclaration _typeDeclaration;
private readonly bool _isContentTypeRange;

public ResponseBodyContentGenerator(string contentType, TypeDeclaration typeDeclaration)
{
_contentType = MediaTypeHeaderValue.Parse(contentType);
_typeDeclaration = typeDeclaration;
ContentPropertyName = contentType.ToPascalCase();

_isContentTypeRange = false;
switch (_contentType.MediaType)
{
case "*/*":
_contentVariableName = "any";
_isContentTypeRange = true;
break;
case not null when _contentType.MediaType.EndsWith("*"):
_contentVariableName = $"any{_contentType.MediaType.TrimEnd('*').TrimEnd('/').ToPascalCase()}";
_isContentTypeRange = true;
break;
case null:
throw new InvalidOperationException("Content type is null");
default:
_contentVariableName = _contentType.MediaType.ToCamelCase();
break;
}

ContentPropertyName = _contentVariableName.ToPascalCase();
}

internal string SchemaLocation => _typeDeclaration.RelativeSchemaLocation;
public string GenerateConstructor(string className, string contentTypeFieldName) =>
$$"""
/// <summary>
/// Construct content for {{contentType}}
/// Construct content for {{_contentType}}
/// </summary>
/// <param name="{{_contentVariableName}}">Content</param>
public {{className}}({{typeDeclaration.FullyQualifiedDotnetTypeName()}} {{_contentVariableName}})
{
/// <param name="{{_contentVariableName}}">Content</param>{{(_isContentTypeRange ? $"""

/// <param name="contentType">Content type must match range {_contentType.MediaType}</param>
""" : "")}}
public {{className}}({{_typeDeclaration.FullyQualifiedDotnetTypeName()}} {{_contentVariableName}}{{(_isContentTypeRange ? ", string contentType" : "")}})
{{{(_isContentTypeRange ?
$$"""

EnsureExpectedContentType(MediaTypeHeaderValue.Parse(contentType), MediaTypeHeaderValue.Parse("{{_contentType}}"));
""" : "")}}
{{ContentPropertyName}} = {{_contentVariableName}};
{{contentTypeFieldName}} = "{{contentType}}";
}
{{contentTypeFieldName}} = {{(_isContentTypeRange ? "contentType" : $"\"{_contentType.MediaType}\"")}};
}
""";

public string GenerateContentProperty()
{
return
$$"""
/// <summary>
/// Content for {{contentType}}
/// Content for {{_contentType}}
/// </summary>
internal {{typeDeclaration.FullyQualifiedDotnetTypeName()}}? {{ContentPropertyName}} { get; }
internal {{_typeDeclaration.FullyQualifiedDotnetTypeName()}}? {{ContentPropertyName}} { get; }
""";
}
}
23 changes: 23 additions & 0 deletions src/OpenAPI.WebApiGenerator/CodeGeneration/ResponseGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public SourceCode GenerateResponseClass(string @namespace, string path)
$$"""
#nullable enable
using Corvus.Json;
using System.Net.Http.Headers;
using System.Text.Json;
using {{httpResponseExtensionsGenerator.Namespace}};

Expand All @@ -35,6 +36,28 @@ internal abstract partial class Response
=> (code >= {{i}}00 && code <= {{i}}99) ? code : throw new InvalidOperationException($"Expected {{i}}xx status code, got {code}");
""")}}

/// <summary>
/// Ensures that the specified content type matches the specification
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified content type does not match the specification</exception>
/// </summary>
/// <param name="contentType">Content type</param>
/// <param name="expectedContentType">Expected content type</param>
protected void EnsureExpectedContentType(MediaTypeHeaderValue contentType, MediaTypeHeaderValue expectedContentType)
{
var valid = expectedContentType.MediaType switch
{
"*/*" => true,
not null when expectedContentType.MediaType.EndsWith("*") =>
contentType.MediaType?.StartsWith(expectedContentType.MediaType.TrimEnd('*'), StringComparison.OrdinalIgnoreCase) ?? false,
not null => contentType.MediaType?.Equals(expectedContentType.MediaType, StringComparison.OrdinalIgnoreCase) ?? false,
_ => false
};

if (valid)
return;
throw new ArgumentOutOfRangeException($"Expected content type {contentType.MediaType} to match range {expectedContentType.MediaType}");
}

/// <summary>
/// Write the response to a http response object
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/OpenAPI.WebApiGenerator/Extensions/MediaTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;

namespace OpenAPI.WebApiGenerator.Extensions;

internal static class MediaTypeExtensions
{
internal static string GetMatchConditionExpression(this MediaTypeHeaderValue value, string mediaTypeVariableName)
{
var expressions = new List<string>();
if (value.MediaType is not null)
{
expressions.Add(value.MediaType switch
{
"*/*" => "true",
not null when value.MediaType.EndsWith("*") =>
$"""{mediaTypeVariableName}.{nameof(value.MediaType)}.{nameof(value.MediaType.StartsWith)}("{value.MediaType.TrimEnd('*')}", StringComparison.OrdinalIgnoreCase)""",
_ =>
$"""{mediaTypeVariableName}.{nameof(value.MediaType)}.{nameof(value.MediaType.Equals)}("{value.MediaType}", StringComparison.OrdinalIgnoreCase)"""
});
}

expressions.AddRange(value.Parameters.Select(parameter =>
$"{mediaTypeVariableName}.{nameof(value.Parameters)}.{nameof(value.Parameters.Contains)}({(parameter.Value is null ?
$"""new NameValueHeaderValue("{parameter.Name}")""" :
$"""new NameValueHeaderValue("{parameter.Name}", "{parameter.Value}")""")})"));

return string.Join(" && ", expressions);
}

internal static int GetPrecedence(this MediaTypeHeaderValue value) =>
value.Parameters.Count + value.MediaType switch
{
"*/*" => 0,
not null when value.MediaType.EndsWith("*") => 100,
_ => 1000
};
}
3 changes: 3 additions & 0 deletions src/OpenAPI.WebApiGenerator/OpenAPI.WebApiGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,7 @@
</ItemGroup>
</Target>

<ItemGroup>
<InternalsVisibleTo Include="OpenAPI.WebApiGenerator.Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal partial Task<Response> HandleAsync(Request request, CancellationToken c
_ = request.Header.Bar;

var response = new Response.OK200(Components.Schemas.FooProperties.Create(
name: request.Body.ApplicationJson?.Name))
name: request.Body.ApplicationJson?.Name), "application/json")
{
Headers = new Response.OK200.ResponseHeaders
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Example.OpenApi32/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}
},
"content": {
"application/json": {
"application/*": {
"schema": {
"$ref": "#/components/schemas/FooProperties"
}
Expand Down
Loading
Loading