diff --git a/Modules/Conversion/Serializers/SerializationRegistry.cs b/Modules/Conversion/Serializers/SerializationRegistry.cs index 206d5771..ab5dd9c4 100644 --- a/Modules/Conversion/Serializers/SerializationRegistry.cs +++ b/Modules/Conversion/Serializers/SerializationRegistry.cs @@ -1,71 +1,81 @@ -using System.Collections.Generic; - -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.Conversion.Providers -{ - - /// - /// Registers formats that can be used to serialize and - /// deserialize objects sent to or received from a - /// service oriented handler. - /// - public sealed class SerializationRegistry - { - - #region Get-/Setters - - private FlexibleContentType Default { get; } - - private Dictionary Formats { get; } - - #endregion - - #region Initialization - - public SerializationRegistry(FlexibleContentType defaultType, - Dictionary formats) - { - Default = defaultType; - Formats = formats; - } - - #endregion - - #region Functionality - - public ISerializationFormat? GetDeserialization(IRequest request) - { - if (request.Headers.TryGetValue("Content-Type", out string? requested)) - { - return GetFormat(FlexibleContentType.Parse(requested)); - } - - return GetFormat(Default); - } - - public ISerializationFormat? GetSerialization(IRequest request) - { - if (request.Headers.TryGetValue("Accept", out string? accepted)) - { - return GetFormat(FlexibleContentType.Parse(accepted)) ?? GetFormat(Default); - } - - return GetFormat(Default); - } - - private ISerializationFormat? GetFormat(FlexibleContentType contentType) - { - if (Formats.TryGetValue(contentType, out var format)) - { - return format; - } - - return null; - } - - #endregion - - } - -} +using System.Collections.Generic; + +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.Conversion.Providers +{ + + /// + /// Registers formats that can be used to serialize and + /// deserialize objects sent to or received from a + /// service oriented handler. + /// + public sealed class SerializationRegistry + { + + #region Get-/Setters + + private FlexibleContentType Default { get; } + + private Dictionary Formats { get; } + + #endregion + + #region Initialization + + public SerializationRegistry(FlexibleContentType defaultType, + Dictionary formats) + { + Default = defaultType; + Formats = formats; + } + + #endregion + + #region Functionality + + public ISerializationFormat? GetDeserialization(IRequest request) + { + if (request.Headers.TryGetValue("Content-Type", out string? requested)) + { + return GetFormat(FlexibleContentType.Parse(requested)); + } + + return GetFormat(Default); + } + + public ISerializationFormat? GetSerialization(IRequest request) + { + if (request.Headers.TryGetValue("Accept", out string? accepted)) + { + return GetFormat(FlexibleContentType.Parse(accepted)) ?? GetFormat(Default); + } + + return GetFormat(Default); + } + + public ISerializationFormat? GetFormat(string? contentType) + { + if (contentType != null) + { + return GetFormat(FlexibleContentType.Parse(contentType)); + } + + return GetFormat(Default); + } + + private ISerializationFormat? GetFormat(FlexibleContentType contentType) + { + if (Formats.TryGetValue(contentType, out var format)) + { + return format; + } + + return null; + } + + #endregion + + } + +} diff --git a/Testing/Acceptance/Testing/ContentTests.cs b/Testing/Acceptance/Testing/ContentTests.cs new file mode 100644 index 00000000..0f41b058 --- /dev/null +++ b/Testing/Acceptance/Testing/ContentTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Reflection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Testing +{ + + [TestClass] + public sealed class ContentTests + { + + public record class MyType(int ID); + + [TestMethod] + public async Task TestDeserialization() + { + var expectation = new MyType(42); + + var handler = Inline.Create() + .Get(() => expectation); + + using var host = TestHost.Run(handler); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual(expectation, await response.GetContentAsync()); + } + + [TestMethod] + public async Task TestNull() + { + var handler = Inline.Create() + .Get(() => (MyType?)null); + + using var host = TestHost.Run(handler); + + using var response = await host.GetResponseAsync(); + + Assert.IsNull(await response.GetOptionalContentAsync()); + } + + [TestMethod] + public async Task TestUnsupported() + { + var handler = Inline.Create() + .Get(() => new Result("Nah").Type(FlexibleContentType.Get("text/html"))); + + using var host = TestHost.Run(handler); + + using var response = await host.GetResponseAsync(); + + await Assert.ThrowsExceptionAsync(async () => + { + await response.GetOptionalContentAsync(); + }); + } + + } + +} diff --git a/Testing/Testing/ContentExtensions.cs b/Testing/Testing/ContentExtensions.cs new file mode 100644 index 00000000..990d4688 --- /dev/null +++ b/Testing/Testing/ContentExtensions.cs @@ -0,0 +1,71 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using GenHTTP.Modules.Conversion; +using GenHTTP.Modules.Protobuf; + +namespace GenHTTP.Testing +{ + + public static class ContentExtensions + { + + /// + /// Reads the response body as a string. + /// + /// The response to read + /// The content of the HTTP response + public static async ValueTask GetContentAsync(this HttpResponseMessage response) => await response.Content.ReadAsStringAsync(); + + /// + /// Deserializes the payload of the HTTP response into the given type. + /// + /// The type of the payload to be read + /// The response to read the payload from + /// The deserialized payload of the response + /// Thrown if the format returned by the server is not supported + /// + /// This method supports all formats that ship with the GenHTTP framework (JSON, XML, form encoded, Protobuf) + /// and falls back to JSON if the server does not indicate a content type. + /// + public static async ValueTask GetContentAsync(this HttpResponseMessage response) + { + return await response.GetOptionalContentAsync() ?? throw new InvalidOperationException("Server did not return a result"); + } + + /// + /// Attempts to deserialize the payload of the HTTP response into the given type. + /// + /// The type of the payload to be read + /// The response to read the payload from + /// The deserialized payload of the response or null, if the server did not return data + /// Thrown if the format returned by the server is not supported + /// + /// This method supports all formats that ship with the GenHTTP framework (JSON, XML, form encoded, Protobuf) + /// and falls back to JSON if the server does not indicate a content type. + /// + public static async ValueTask GetOptionalContentAsync(this HttpResponseMessage response) + { + if (response.StatusCode == HttpStatusCode.NoContent) + { + return default; + } + + var type = response.GetContentHeader("Content-Type"); + + var registry = Serialization.Default() + .AddProtobuf() + .Build(); + + var format = registry.GetFormat(type) ?? throw new InvalidOperationException($"Unable to find deserializer for content type '{type}'"); + + using var body = await response.Content.ReadAsStreamAsync(); + + return (T?)await format.DeserializeAsync(body, typeof(T)); + } + + } + +} diff --git a/Testing/Testing/GenHTTP.Testing.csproj b/Testing/Testing/GenHTTP.Testing.csproj index 1a5e77ae..d40c8c24 100644 --- a/Testing/Testing/GenHTTP.Testing.csproj +++ b/Testing/Testing/GenHTTP.Testing.csproj @@ -48,6 +48,8 @@ + + diff --git a/Testing/Testing/TestExtensions.cs b/Testing/Testing/HeaderExtensions.cs similarity index 60% rename from Testing/Testing/TestExtensions.cs rename to Testing/Testing/HeaderExtensions.cs index 9844c72d..bce03541 100644 --- a/Testing/Testing/TestExtensions.cs +++ b/Testing/Testing/HeaderExtensions.cs @@ -1,40 +1,32 @@ -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; - -namespace GenHTTP.Testing -{ - - public static class TestExtensions - { - - /// - /// Reads the response body as a string. - /// - /// The response to read - /// The content of the HTTP response - public static async ValueTask GetContentAsync(this HttpResponseMessage response) => await response.Content.ReadAsStringAsync(); - - public static string? GetHeader(this HttpResponseMessage response, string key) - { - if (response.Headers.TryGetValues(key, out var values)) - { - return values.FirstOrDefault(); - } - - return null; - } - - public static string? GetContentHeader(this HttpResponseMessage response, string key) - { - if (response.Content.Headers.TryGetValues(key, out var values)) - { - return values.FirstOrDefault(); - } - - return null; - } - - } - -} +using System.Linq; +using System.Net.Http; + +namespace GenHTTP.Testing +{ + + public static class HeaderExtensions + { + + public static string? GetHeader(this HttpResponseMessage response, string key) + { + if (response.Headers.TryGetValues(key, out var values)) + { + return values.FirstOrDefault(); + } + + return null; + } + + public static string? GetContentHeader(this HttpResponseMessage response, string key) + { + if (response.Content.Headers.TryGetValues(key, out var values)) + { + return values.FirstOrDefault(); + } + + return null; + } + + } + +}