Skip to content

Commit

Permalink
feat(repeater): support stringified enum value serialization
Browse files Browse the repository at this point in the history
closes #170
  • Loading branch information
ostridm committed May 27, 2024
1 parent ea763bd commit 82502d2
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using MessagePack;
using MessagePack.Formatters;

namespace SecTester.Repeater.Bus.Formatters;

// ADHOC: MessagePack-CSharp prohibits declaration of IMessagePackFormatter<T> requesting to use System.Enum instead, refer to formatter interface argument type check
// https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/db2320b3338735c9266110bbbfffe63f17dfdf46/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Resolvers/DynamicObjectResolver.cs#L623

public class MessagePackStringEnumFormatter<T> : IMessagePackFormatter<Enum>
where T : Enum
{
private static readonly Dictionary<T, string> EnumToString = typeof(T)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(field => new
{
Value = (T)field.GetValue(null),
StringValue = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? field.Name
})
.ToDictionary(x => x.Value, x => x.StringValue);

private static readonly Dictionary<string, T> StringToEnum = EnumToString.ToDictionary(x => x.Value, x => x.Key);

public void Serialize(ref MessagePackWriter writer, Enum value, MessagePackSerializerOptions options)
{
if (!EnumToString.TryGetValue((T)value, out var stringValue))
{
throw new MessagePackSerializationException($"No string representation found for {value}");
}

writer.Write(stringValue);
}

public Enum Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{

var stringValue = reader.ReadString();

if (!StringToEnum.TryGetValue(stringValue, out var enumValue))

Check warning on line 43 in src/SecTester.Repeater/Bus/Formatters/MessagePackStringEnumFormatter.cs

View workflow job for this annotation

GitHub Actions / windows-2019

Possible null reference argument for parameter 'key' in 'bool Dictionary<string, T>.TryGetValue(string key, out T value)'.
{
throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}.");
}

return enumValue;
}
}
13 changes: 12 additions & 1 deletion src/SecTester.Repeater/Bus/IncomingRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Serialization;
using MessagePack;
using SecTester.Repeater.Bus.Formatters;
using SecTester.Repeater.Runners;
Expand All @@ -15,6 +17,15 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest
private const string UrlKey = "url";
private const string MethodKey = "method";

private static readonly Dictionary<string, Protocol> ProtocolEntries = typeof(Protocol)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(field => new
{
Value = (Protocol)field.GetValue(null),
StringValue = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? field.Name
})
.ToDictionary(x => x.StringValue, x => x.Value);

[Key(MethodKey)]
[MessagePackFormatter(typeof(MessagePackHttpMethodFormatter))]
public HttpMethod Method { get; set; } = HttpMethod.Get;
Expand All @@ -24,7 +35,7 @@ public record IncomingRequest(Uri Url) : HttpMessage, IRequest

public static IncomingRequest FromDictionary(Dictionary<object, object> dictionary)
{
var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && Enum.TryParse<Protocol>(p.ToString(), out var e)
var protocol = dictionary.TryGetValue(ProtocolKey, out var p) && p is string && ProtocolEntries.TryGetValue(p.ToString(), out var e)
? e
: Protocol.Http;

Expand Down
3 changes: 3 additions & 0 deletions src/SecTester.Repeater/Protocol.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Runtime.Serialization;

namespace SecTester.Repeater;

public enum Protocol
{
[EnumMember(Value = "http")]
Http
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,52 @@ public record HttpHeadersDto

private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;

private static IEnumerable<HttpHeadersDto> Fixtures => new[]
public static readonly IEnumerable<object[]> Fixtures = new List<object[]>()
{
new HttpHeadersDto
new object[]
{
Headers = null
new HttpHeadersDto
{
Headers = null
}
},
new HttpHeadersDto
new object[]
{
Headers = new List<KeyValuePair<string, IEnumerable<string>>>()
new HttpHeadersDto
{
Headers = new List<KeyValuePair<string, IEnumerable<string>>>()
}
},
new HttpHeadersDto
new object[]
{
Headers = new List<KeyValuePair<string, IEnumerable<string>>>
new HttpHeadersDto
{
new("content-type", new List<string> { "application/json" }),
new("cache-control", new List<string> { "no-cache", "no-store" })
Headers = new List<KeyValuePair<string, IEnumerable<string>>>
{
new("content-type", new List<string> { "application/json" }),
new("cache-control", new List<string> { "no-cache", "no-store" })
}
}
}
};

public static readonly IEnumerable<string> WrongFormatFixtures = new[]
public static readonly IEnumerable<object[]> WrongValueFixtures = new List<object[]>()
{
"{\"headers\":5}",
"{\"headers\":[]}",
"{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}",
"{\"headers\":{\"content-type\":1}}",
"{\"headers\":{\"content-type\":[null]}}",
"{\"headers\":{\"content-type\":[1]}}"
};

public static IEnumerable<object?[]> SerializeDeserializeFixtures => Fixtures
.Select((x) => new object?[]
new object[]
{
x, x
});
"{\"headers\":5}"
},
new object[] { "{\"headers\":[]}" },
new object[] { "{\"headers\":{\"content-type\":{\"foo\"}:{\"bar\"}}}" },
new object[] { "{\"headers\":{\"content-type\":1}}" },
new object[] { "{\"headers\":{\"content-type\":[null]}}" },
new object[] { "{\"headers\":{\"content-type\":[1]}}" }
};

[Theory]
[MemberData(nameof(SerializeDeserializeFixtures))]
[MemberData(nameof(Fixtures))]
public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue(
HttpHeadersDto input,
HttpHeadersDto expected)
HttpHeadersDto input)
{
// arrange
var serialized = MessagePackSerializer.Serialize(input, Options);
Expand All @@ -64,7 +69,7 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyDeseriali
var result = MessagePackSerializer.Deserialize<HttpHeadersDto>(serialized, Options);

// assert
result.Should().BeEquivalentTo(expected);
result.Should().BeEquivalentTo(input);
}

[Fact]
Expand All @@ -83,15 +88,9 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldCorrectlyHandleMis
});
}

public static IEnumerable<object?[]> ThrowWhenWrongFormatFixtures => WrongFormatFixtures
.Select((x) => new object?[]
{
x
});

[Theory]
[MemberData(nameof(ThrowWhenWrongFormatFixtures))]
public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow(
[MemberData(nameof(WrongValueFixtures))]
public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(
string input)
{
// arrange
Expand All @@ -102,6 +101,6 @@ public void HttpHeadersMessagePackFormatter_Deserialize_ShouldThrow(

// assert
act.Should().Throw<MessagePackSerializationException>().WithMessage(
"Failed to deserialize SecTester.Repeater.Tests.Bus.Formatters.MessagePackHttpHeadersFormatterTests+HttpHeadersDto value.");
"Failed to deserialize*");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,35 @@ public record HttpMethodDto

private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;

private static IEnumerable<HttpMethodDto>
Fixtures =>
new[]
public static readonly IEnumerable<object[]> Fixtures = new List<object[]>()
{
new object[]
{
new HttpMethodDto
{
Method = null
},
}
},
new object[]
{
new HttpMethodDto
{
Method = HttpMethod.Get
},
}
},
new object[]
{
new HttpMethodDto
{
Method = new HttpMethod("PROPFIND")
}
};

public static IEnumerable<object?[]> SerializeDeserializeFixtures => Fixtures
.Select((x) => new object?[]
{
x, x
});
}
};

[Theory]
[MemberData(nameof(SerializeDeserializeFixtures))]
[MemberData(nameof(Fixtures))]
public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializePreviouslySerializedValue(
HttpMethodDto input,
HttpMethodDto expected)
HttpMethodDto input)
{
// arrange
var serialized = MessagePackSerializer.Serialize(input, Options);
Expand All @@ -52,7 +52,7 @@ public void HttpMethodMessagePackFormatter_Deserialize_ShouldCorrectlyDeserializ
var result = MessagePackSerializer.Deserialize<HttpMethodDto>(serialized, Options);

// assert
result.Should().BeEquivalentTo(expected);
result.Should().BeEquivalentTo(input);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Runtime.Serialization;
using MessagePack;
using SecTester.Repeater.Bus.Formatters;

namespace SecTester.Repeater.Tests.Bus.Formatters;

public sealed class MessagePackStringEnumFormatterTests
{
public enum Foo
{
[EnumMember(Value = "bar")]
Bar = 0,
[EnumMember(Value = "baz_cux")]
BazCux = 1
}

[MessagePackObject]
public record EnumDto
{
[Key("foo")]
[MessagePackFormatter(typeof(MessagePackStringEnumFormatter<Foo>))]
public Enum Foo { get; set; }
}

private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard;

public static IEnumerable<object[]>
Fixtures =>
new List<object[]>
{
new object[] { "{\"foo\":\"bar\"}" },
new object[] { "{\"foo\":\"baz_cux\"}" }
};

public static IEnumerable<object[]>
WrongValueFixtures =>
new List<object[]>
{
new object[] { "{\"foo\": null}" },
new object[] { "{\"foo\": 5}" },
new object[] { "{\"foo\":\"BazCux\"}" }
};


[Theory]
[MemberData(nameof(Fixtures))]
public void MessagePackStringEnumFormatter_Serialize_ShouldCorrectlySerialize(
string input)
{
// arrange
var binary = MessagePackSerializer.ConvertFromJson(input, Options);
var obj = MessagePackSerializer.Deserialize<EnumDto>(binary, Options);


// act
var result = MessagePackSerializer.SerializeToJson(obj, Options);

// assert
result.Should().BeEquivalentTo(input);
}

[Theory]
[MemberData(nameof(WrongValueFixtures))]
public void MessagePackStringEnumFormatter_Deserialize_ShouldThrowWhenDataHasWrongValue(string input)
{
// arrange
var binary = MessagePackSerializer.ConvertFromJson(input, Options);

// act
var act = () => MessagePackSerializer.Deserialize<EnumDto>(binary, Options);

// assert
act.Should().Throw<MessagePackSerializationException>().WithMessage(
"Failed to deserialize*");
}
}
2 changes: 1 addition & 1 deletion test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot
{
// arrange
var packJson =
"{\"type\":2,\"data\":[\"request\",{\"protocol\":0,\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}";
"{\"type\":2,\"data\":[\"request\",{\"protocol\":\"http:\",\"headers\":{\"content-type\":\"application/json\",\"cache-control\":[\"no-cache\",\"no-store\"]},\"body\":\"{\\\"foo\\\":\\\"bar\\\"}\",\"method\":\"PROPFIND\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}";

var serializer = new SocketIOMessagePackSerializer(MessagePackSerializerOptions.Standard);

Expand Down

0 comments on commit 82502d2

Please sign in to comment.