From 23c8ddec64e8410a04330ed23e16b926ed7b646c Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Sat, 8 Jun 2024 01:58:24 +0400 Subject: [PATCH] refactor(repeater): simplify `FromDictionary` method --- src/SecTester.Repeater/Bus/IncomingRequest.cs | 95 ++++++++----------- .../Bus/OutgoingResponse.cs | 10 +- .../DefaultMessagePackSerializerOptions.cs | 2 +- .../Internal/HttpMethods.cs | 30 ++++++ .../MessagePackHttpMethodFormatter.cs | 27 +----- .../MessagePackStringEnumMemberFormatter.cs | 12 +-- .../SecTester.Repeater.csproj | 2 + .../Bus/IncomingRequestTests.cs | 24 +---- .../MessagePackHttpHeadersFormatterTests.cs | 9 +- 9 files changed, 84 insertions(+), 127 deletions(-) create mode 100644 src/SecTester.Repeater/Internal/HttpMethods.cs diff --git a/src/SecTester.Repeater/Bus/IncomingRequest.cs b/src/SecTester.Repeater/Bus/IncomingRequest.cs index 0d1928e..84e2173 100644 --- a/src/SecTester.Repeater/Bus/IncomingRequest.cs +++ b/src/SecTester.Repeater/Bus/IncomingRequest.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.Http; -using System.Reflection; -using System.Runtime.Serialization; using MessagePack; using SecTester.Repeater.Internal; using SecTester.Repeater.Runners; @@ -20,75 +17,57 @@ public record IncomingRequest(Uri Url) : IRequest private const string BodyKey = "body"; private const string ProtocolKey = "protocol"; - private static readonly Dictionary ProtocolEntries = typeof(Protocol) - .GetFields(BindingFlags.Public | BindingFlags.Static) - .Select(field => new - { - Value = (Protocol)field.GetValue(null), - StringValue = field.GetCustomAttribute()?.Value ?? field.Name - }) - .ToDictionary(x => MessagePackNamingPolicy.SnakeCase.ConvertName(x.StringValue), x => x.Value); - - [Key(ProtocolKey)] - public Protocol Protocol { get; set; } = Protocol.Http; - - private IEnumerable>> _headers = Enumerable.Empty>>(); + [Key(ProtocolKey)] public Protocol Protocol { get; set; } = Protocol.Http; - [Key(HeadersKey)] - public IEnumerable>> Headers - { - get => _headers; - // ADHOC: convert from a kind of assignable type to formatter resolvable type - set => _headers = value.AsEnumerable(); - } + [Key(HeadersKey)] public IEnumerable>> Headers { get; set; } = new Dictionary>(); - [Key(BodyKey)] - public string? Body { get; set; } + [Key(BodyKey)] public string? Body { get; set; } - [Key(MethodKey)] - public HttpMethod Method { get; set; } = HttpMethod.Get; + [Key(MethodKey)] public HttpMethod Method { get; set; } = HttpMethod.Get; - [Key(UrlKey)] - public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); + [Key(UrlKey)] public Uri Url { get; set; } = Url ?? throw new ArgumentNullException(nameof(Url)); public static IncomingRequest FromDictionary(Dictionary dictionary) { - var protocol = !dictionary.ContainsKey(ProtocolKey) || (dictionary.TryGetValue(ProtocolKey, out var p1) && p1 is null) - ? Protocol.Http - : dictionary.TryGetValue(ProtocolKey, out var p2) && p2 is string && ProtocolEntries.TryGetValue(p2.ToString(), out var e) - ? e - : throw new InvalidDataException(FormatPropertyError(ProtocolKey)); - - var uri = dictionary.TryGetValue(UrlKey, out var u) && u is string - ? new Uri(u.ToString()) - : throw new InvalidDataException(FormatPropertyError(UrlKey)); - - var method = dictionary.TryGetValue(MethodKey, out var m) && m is string - ? new HttpMethod(m.ToString()) + var protocol = dictionary.TryGetValue(ProtocolKey, out var protocolObj) && protocolObj is string protocolStr + ? (Protocol)Enum.Parse(typeof(Protocol), protocolStr, true) + : Protocol.Http; + + var headers = dictionary.TryGetValue(HeadersKey, out var headersObj) && headersObj is Dictionary headersDict + ? ConvertToHeaders(headersDict) + : new Dictionary>(); + + var body = dictionary.TryGetValue(BodyKey, out var bodyObj) + ? bodyObj?.ToString() + : null; + + var method = dictionary.TryGetValue(MethodKey, out var methodObj) && methodObj is string methodStr + ? HttpMethods.Items.TryGetValue(methodStr, out var m) && m is not null + ? m + : HttpMethod.Get : HttpMethod.Get; - var body = dictionary.TryGetValue(BodyKey, out var b) && b is string ? b.ToString() : null; - - var headers = dictionary.TryGetValue(HeadersKey, out var h) && h is Dictionary value - ? MapHeaders(value) - : Enumerable.Empty>>(); + var url = dictionary.TryGetValue(UrlKey, out var urlObj) && urlObj is string urlStr + ? new Uri(urlStr) + : null; - return new IncomingRequest(uri) + return new IncomingRequest(url!) { Protocol = protocol, + Headers = headers, Body = body, - Method = method, - Headers = headers + Method = method }; } - private static IEnumerable>> MapHeaders(Dictionary headers) => - headers.Select(kvp => kvp.Value switch - { - IEnumerable strings => new KeyValuePair>(kvp.Key.ToString(), strings.Select(x => x.ToString())), - null => new KeyValuePair>(kvp.Key.ToString(), Enumerable.Empty()), - _ => new KeyValuePair>(kvp.Key.ToString(), new[] { kvp.Value.ToString() }) - }); - - private static string FormatPropertyError(string propName) => $"{propName} is either null or has an invalid data type or value"; + private static IEnumerable>> ConvertToHeaders(Dictionary headers) => + headers.ToDictionary( + kvp => kvp.Key.ToString()!, + kvp => kvp.Value switch + { + IEnumerable list => list.Select(v => v.ToString()!), + string str => new[] { str }, + _ => Enumerable.Empty() + } + ); } diff --git a/src/SecTester.Repeater/Bus/OutgoingResponse.cs b/src/SecTester.Repeater/Bus/OutgoingResponse.cs index 9378a4a..70a8658 100644 --- a/src/SecTester.Repeater/Bus/OutgoingResponse.cs +++ b/src/SecTester.Repeater/Bus/OutgoingResponse.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using MessagePack; using SecTester.Repeater.Runners; @@ -23,13 +22,6 @@ public record OutgoingResponse : IResponse [Key("errorCode")] public string? ErrorCode { get; set; } - private IEnumerable>> _headers = Enumerable.Empty>>(); - [Key("headers")] - public IEnumerable>> Headers - { - get => _headers; - // ADHOC: convert from a kind of assignable type to formatter resolvable type - set => _headers = value.AsEnumerable(); - } + public IEnumerable>> Headers { get; set; } = new Dictionary>(); } diff --git a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs index 7e341b6..707b476 100644 --- a/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs +++ b/src/SecTester.Repeater/Internal/DefaultMessagePackSerializerOptions.cs @@ -3,7 +3,7 @@ namespace SecTester.Repeater.Internal; -internal class DefaultMessagePackSerializerOptions +internal static class DefaultMessagePackSerializerOptions { internal static readonly MessagePackSerializerOptions Instance = new( CompositeResolver.Create( diff --git a/src/SecTester.Repeater/Internal/HttpMethods.cs b/src/SecTester.Repeater/Internal/HttpMethods.cs new file mode 100644 index 0000000..1900399 --- /dev/null +++ b/src/SecTester.Repeater/Internal/HttpMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; + +namespace SecTester.Repeater.Internal; + +public class HttpMethods +{ + public static IDictionary Items { get; } = typeof(HttpMethod) + .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) + .Select(x => x.GetValue(null)) + .Cast() + .Concat(new List + { + new("PATCH"), + new("COPY"), + new("LINK"), + new("UNLINK"), + new("PURGE"), + new("LOCK"), + new("UNLOCK"), + new("PROPFIND"), + new("VIEW") + }) + .Distinct() + .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); +} diff --git a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs index d362a64..0fc6569 100644 --- a/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs +++ b/src/SecTester.Repeater/Internal/MessagePackHttpMethodFormatter.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Reflection; using MessagePack; using MessagePack.Formatters; @@ -10,27 +6,6 @@ namespace SecTester.Repeater.Internal; internal class MessagePackHttpMethodFormatter : IMessagePackFormatter { - private static readonly IEnumerable BaseMethods = typeof(HttpMethod) - .GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Where(x => x.PropertyType.IsAssignableFrom(typeof(HttpMethod))) - .Select(x => x.GetValue(null)) - .Cast(); - - private static readonly IEnumerable CustomMethods = new List - { - new("PATCH"), - new("COPY"), - new("LINK"), - new("UNLINK"), - new("PURGE"), - new("LOCK"), - new("UNLOCK"), - new("PROPFIND"), - new("VIEW") - }; - - private static readonly IDictionary Methods = BaseMethods.Concat(CustomMethods).Distinct() - .ToDictionary(x => x.Method, x => x, StringComparer.InvariantCultureIgnoreCase); public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePackSerializerOptions options) { if (null == value) @@ -63,7 +38,7 @@ public void Serialize(ref MessagePackWriter writer, HttpMethod? value, MessagePa { var token = reader.ReadString(); - if (token is null || !Methods.TryGetValue(token, out var method)) + if (token is null || !HttpMethods.Items.TryGetValue(token, out var method)) { throw new MessagePackSerializationException( $"Unexpected value {token} when parsing the {nameof(HttpMethod)}."); diff --git a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs index 1db48fa..064e40c 100644 --- a/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs +++ b/src/SecTester.Repeater/Internal/MessagePackStringEnumMemberFormatter.cs @@ -20,18 +20,18 @@ internal class MessagePackStringEnumMemberFormatter : IMessagePackFormatter x.Value, x => x.StringValue); - private readonly Dictionary CasedStringToEnum; - private readonly Dictionary CasedEnumToString; + private readonly Dictionary _casedStringToEnum; + private readonly Dictionary _casedEnumToString; public MessagePackStringEnumMemberFormatter(MessagePackNamingPolicy namingPolicy) { - this.CasedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); - this.CasedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); + this._casedEnumToString = EnumToString.ToDictionary(x => x.Key, x => namingPolicy.ConvertName(x.Value)); + this._casedStringToEnum = EnumToString.ToDictionary(x => namingPolicy.ConvertName(x.Value), x => x.Key); } public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) { - if (!CasedEnumToString.TryGetValue(value, out var stringValue)) + if (!_casedEnumToString.TryGetValue(value, out var stringValue)) { throw new MessagePackSerializationException($"No string representation found for {value}"); } @@ -43,7 +43,7 @@ public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions { var stringValue = reader.ReadString(); - if (null == stringValue || !CasedStringToEnum.TryGetValue(stringValue, out var enumValue)) + if (null == stringValue || !_casedStringToEnum.TryGetValue(stringValue, out var enumValue)) { throw new MessagePackSerializationException($"Unable to parse '{stringValue}' to {typeof(T).Name}."); } diff --git a/src/SecTester.Repeater/SecTester.Repeater.csproj b/src/SecTester.Repeater/SecTester.Repeater.csproj index 79094ef..1bbf68f 100644 --- a/src/SecTester.Repeater/SecTester.Repeater.csproj +++ b/src/SecTester.Repeater/SecTester.Repeater.csproj @@ -17,7 +17,9 @@ + + diff --git a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs index e193382..672bcc3 100644 --- a/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs +++ b/test/SecTester.Repeater.Tests/Bus/IncomingRequestTests.cs @@ -56,7 +56,7 @@ public void IncomingRequest_FromDictionary_ShouldCreateInstance(IncomingRequest } [Fact] - public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNotProvided() + public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsInvalid() { // arrange var packJson = @@ -72,7 +72,7 @@ public void IncomingRequest_FromDictionary_ShouldThrowWhenRequiredPropertyWasNot var act = () => IncomingRequest.FromDictionary(deserializedDictionary); // assert - act.Should().Throw(); + act.Should().Throw(); } [Fact] @@ -114,24 +114,4 @@ public void IncomingRequest_FromDictionary_ShouldParseProtocolValue() // assert result.Should().BeEquivalentTo(new IncomingRequest(new Uri("https://foo.bar/1")) { Protocol = Protocol.Http }); } - - [Fact] - public void IncomingRequest_FromDictionary_ShouldThrowWhenProtocolIsNotParsable() - { - // arrange - var packJson = - "{\"type\":2,\"data\":[\"request\",{\"protocol\":\"ws\",\"url\":\"https://foo.bar/1\"}],\"options\":{\"compress\":true},\"id\":1,\"nsp\":\"/some\"}"; - - var serializer = new SocketIOMessagePackSerializer(Options); - - var deserializedPackMessage = MessagePackSerializer.Deserialize(MessagePackSerializer.ConvertFromJson(packJson), Options); - - var deserializedDictionary = serializer.Deserialize>(deserializedPackMessage, 1); - - // act - var act = () => IncomingRequest.FromDictionary(deserializedDictionary); - - // assert - act.Should().Throw(); - } } diff --git a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs index 59f20d4..68d29cf 100644 --- a/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs +++ b/test/SecTester.Repeater.Tests/Internal/MessagePackHttpHeadersFormatterTests.cs @@ -13,24 +13,23 @@ public sealed class MessagePackHttpHeadersFormatterTests ) ); - public static readonly IEnumerable Fixtures = new List() + public static readonly IEnumerable Fixtures = new List() { - new object[] + new object?[] { null }, new object[] { Enumerable.Empty>>() - }, - new object[] + new object?[] { new List>> { new("content-type", new List { "application/json" }), new("cache-control", new List { "no-cache", "no-store" }) - }.AsEnumerable() + } } };