Skip to content

Commit

Permalink
Add test extensions to easily deserialize responses (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Jul 1, 2024
1 parent 9dd3e31 commit 938e559
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 111 deletions.
152 changes: 81 additions & 71 deletions Modules/Conversion/Serializers/SerializationRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,71 +1,81 @@
using System.Collections.Generic;

using GenHTTP.Api.Protocol;

namespace GenHTTP.Modules.Conversion.Providers
{

/// <summary>
/// Registers formats that can be used to serialize and
/// deserialize objects sent to or received from a
/// service oriented handler.
/// </summary>
public sealed class SerializationRegistry
{

#region Get-/Setters

private FlexibleContentType Default { get; }

private Dictionary<FlexibleContentType, ISerializationFormat> Formats { get; }

#endregion

#region Initialization

public SerializationRegistry(FlexibleContentType defaultType,
Dictionary<FlexibleContentType, ISerializationFormat> 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
{

/// <summary>
/// Registers formats that can be used to serialize and
/// deserialize objects sent to or received from a
/// service oriented handler.
/// </summary>
public sealed class SerializationRegistry
{

#region Get-/Setters

private FlexibleContentType Default { get; }

private Dictionary<FlexibleContentType, ISerializationFormat> Formats { get; }

#endregion

#region Initialization

public SerializationRegistry(FlexibleContentType defaultType,
Dictionary<FlexibleContentType, ISerializationFormat> 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

}

}
63 changes: 63 additions & 0 deletions Testing/Acceptance/Testing/ContentTests.cs
Original file line number Diff line number Diff line change
@@ -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<MyType>());
}

[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<MyType>());
}

[TestMethod]
public async Task TestUnsupported()
{
var handler = Inline.Create()
.Get(() => new Result<string>("Nah").Type(FlexibleContentType.Get("text/html")));

using var host = TestHost.Run(handler);

using var response = await host.GetResponseAsync();

await Assert.ThrowsExceptionAsync<InvalidOperationException>(async () =>
{
await response.GetOptionalContentAsync<MyType>();
});
}

}

}
71 changes: 71 additions & 0 deletions Testing/Testing/ContentExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{

/// <summary>
/// Reads the response body as a string.
/// </summary>
/// <param name="response">The response to read</param>
/// <returns>The content of the HTTP response</returns>
public static async ValueTask<string> GetContentAsync(this HttpResponseMessage response) => await response.Content.ReadAsStringAsync();

/// <summary>
/// Deserializes the payload of the HTTP response into the given type.
/// </summary>
/// <typeparam name="T">The type of the payload to be read</typeparam>
/// <param name="response">The response to read the payload from</param>
/// <returns>The deserialized payload of the response</returns>
/// <exception cref="InvalidOperationException">Thrown if the format returned by the server is not supported</exception>
/// <remarks>
/// 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.
/// </remarks>
public static async ValueTask<T> GetContentAsync<T>(this HttpResponseMessage response)
{
return await response.GetOptionalContentAsync<T>() ?? throw new InvalidOperationException("Server did not return a result");
}

/// <summary>
/// Attempts to deserialize the payload of the HTTP response into the given type.
/// </summary>
/// <typeparam name="T">The type of the payload to be read</typeparam>
/// <param name="response">The response to read the payload from</param>
/// <returns>The deserialized payload of the response or null, if the server did not return data</returns>
/// <exception cref="InvalidOperationException">Thrown if the format returned by the server is not supported</exception>
/// <remarks>
/// 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.
/// </remarks>
public static async ValueTask<T?> GetOptionalContentAsync<T>(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));
}

}

}
2 changes: 2 additions & 0 deletions Testing/Testing/GenHTTP.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
<ProjectReference Include="..\..\Modules\Basics\GenHTTP.Modules.Basics.csproj" />
<ProjectReference Include="..\..\Modules\Layouting\GenHTTP.Modules.Layouting.csproj" />
<ProjectReference Include="..\..\Modules\Practices\GenHTTP.Modules.Practices.csproj" />
<ProjectReference Include="..\..\Modules\Conversion\GenHTTP.Modules.Conversion.csproj" />
<ProjectReference Include="..\..\Modules\Protobuf\GenHTTP.Modules.Protobuf.csproj" />

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace GenHTTP.Testing
{

public static class TestExtensions
{

/// <summary>
/// Reads the response body as a string.
/// </summary>
/// <param name="response">The response to read</param>
/// <returns>The content of the HTTP response</returns>
public static async ValueTask<string> 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;
}

}

}

0 comments on commit 938e559

Please sign in to comment.