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
32 changes: 21 additions & 11 deletions src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace ByteBard.AsyncAPI.Bindings.Http
{
using System;
using System.Net;
using ByteBard.AsyncAPI.Models;
using ByteBard.AsyncAPI.Readers;
using ByteBard.AsyncAPI.Readers.ParseNodes;
Expand All @@ -25,26 +24,37 @@ public class HttpMessageBinding : MessageBinding<HttpMessageBinding>

/// <summary>
/// The HTTP response status code according to RFC 9110. `statusCode` is only relevant for messages referenced by the Operation Reply Object.
/// Stored as an integer so that codes without a <see cref="System.Net.HttpStatusCode"/> member (e.g. 425, 499, 520) are represented as-is.
/// Note: This field is only serialized in AsyncAPI V3.
/// </summary>
public HttpStatusCode? StatusCode { get; set; }
public int? StatusCode { get; set; }

public override string BindingKey => "http";

protected override FixedFieldMap<HttpMessageBinding> FixedFieldMap => new()
{
{ "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } },
{ "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } },
{ "statusCode", (a, n) =>
{
if (int.TryParse(n.GetScalarValue(), out var code))
{
a.StatusCode = (HttpStatusCode)code;
}
}
},
{ "statusCode", (a, n) => { a.StatusCode = LoadStatusCode(n); } },
};

/// <summary>
/// Reads the status code, reporting values outside the RFC 9110 range (100-599) as errors.
/// </summary>
/// <remarks>An out-of-range value is still kept so the document can be written back unchanged.</remarks>
private static int LoadStatusCode(ParseNode node)
{
var code = node.GetIntegerValue();
if (code < 100 || code > 599)
{
node.Context.Diagnostic.Errors.Add(new AsyncApiError(
node.Context.GetLocation(),
$"statusCode {code} is not a valid HTTP status code. It must be between 100 and 599."));
}

return code;
}

public override void SerializeV2(IAsyncApiWriter writer)
{
if (writer is null)
Expand All @@ -71,7 +81,7 @@ public override void SerializeV3(IAsyncApiWriter writer)

if (this.StatusCode.HasValue)
{
writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, (int)this.StatusCode.Value);
writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, this.StatusCode.Value);
}

writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V3BindingVersion);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace ByteBard.AsyncAPI.Tests.Bindings.Http
{
using System.Linq;
using System.Net;
using FluentAssertions;
using ByteBard.AsyncAPI.Bindings;
using ByteBard.AsyncAPI.Bindings.Http;
Expand Down Expand Up @@ -156,7 +155,7 @@ public void V3_HttpMessageBinding_IncludesStatusCode()
{
Description = "response headers",
},
StatusCode = HttpStatusCode.OK,
StatusCode = 200,
});

// Act
Expand All @@ -179,7 +178,7 @@ public void V2_HttpMessageBinding_OmitsStatusCode()
{
Description = "response headers",
},
StatusCode = HttpStatusCode.OK,
StatusCode = 200,
});

// Act
Expand All @@ -189,5 +188,79 @@ public void V2_HttpMessageBinding_OmitsStatusCode()
actual.Should().NotContain("statusCode");
actual.Should().Contain("bindingVersion: 0.2.0");
}

[Test]
public void V3_HttpMessageBinding_ReadsStatusCode()
{
var yaml =
"""
bindings:
http:
statusCode: 404
""";

var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);

diagnostics.Errors.Should().BeEmpty();
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(404);
}

[TestCase("not-found")]
[TestCase("200.5")]
public void V3_HttpMessageBinding_WithInvalidStatusCode_ReportsError(string statusCode)
{
var yaml =
"bindings:\n" +
" http:\n" +
" statusCode: " + statusCode + "\n";

var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);

diagnostics.Errors.Should().ContainSingle().Which.Message.Should().Contain("could not parse to integer");
}

[TestCase(100)]
[TestCase(599)]
[TestCase(425)] // Too Early (RFC 8470), not a System.Net.HttpStatusCode member
[TestCase(499)] // nginx client closed request
[TestCase(520)] // Cloudflare unknown error
public void V3_HttpMessageBinding_WithValidStatusCode_ReportsNoErrorAndRoundTrips(int statusCode)
{
var yaml =
"bindings:\n" +
" http:\n" +
" statusCode: " + statusCode + "\n";

var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);

diagnostics.Errors.Should().BeEmpty();
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode);
message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode);
}

[TestCase(0)]
[TestCase(42)]
[TestCase(99)]
[TestCase(600)]
[TestCase(-200)]
public void V3_HttpMessageBinding_WithOutOfRangeStatusCode_ReportsErrorAndKeepsValue(int statusCode)
{
var yaml =
"bindings:\n" +
" http:\n" +
" statusCode: " + statusCode + "\n";

var settings = new AsyncApiReaderSettings { Bindings = BindingsCollection.Http };
var message = new AsyncApiStringReader(settings).ReadFragment<AsyncApiMessage>(yaml, AsyncApiVersion.AsyncApi3_0, out var diagnostics);

var error = diagnostics.Errors.Should().ContainSingle().Subject;
error.Message.Should().Contain("must be between 100 and 599");
error.Pointer.Should().EndWith("statusCode");
((HttpMessageBinding)message.Bindings["http"]).StatusCode.Should().Be(statusCode);
message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain("statusCode: " + statusCode);
}
}
}
Loading