diff --git a/CryptoExchange.Net.UnitTests/ConverterTests.cs b/CryptoExchange.Net.UnitTests/ConverterTests.cs index b2022c8..2820b97 100644 --- a/CryptoExchange.Net.UnitTests/ConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/ConverterTests.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Converters; using Newtonsoft.Json; using NUnit.Framework; using System; @@ -51,6 +52,44 @@ namespace CryptoExchange.Net.UnitTests var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}"); Assert.AreEqual(output.Time, null); } + + // TODO add tests for ToMilliseconds static methods + + [TestCase(TestEnum.One, "1")] + [TestCase(TestEnum.Two, "2")] + [TestCase(TestEnum.Three, "three")] + [TestCase(TestEnum.Four, "Four")] + [TestCase(null, null)] + public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected) + { + var output = EnumConverter.GetString(value); + Assert.AreEqual(output, expected); + } + + [TestCase(TestEnum.One, "1")] + [TestCase(TestEnum.Two, "2")] + [TestCase(TestEnum.Three, "three")] + [TestCase(TestEnum.Four, "Four")] + public void TestEnumConverterGetStringTests(TestEnum value, string expected) + { + var output = EnumConverter.GetString(value); + Assert.AreEqual(output, expected); + } + + [TestCase("1", TestEnum.One)] + [TestCase("2", TestEnum.Two)] + [TestCase("3", TestEnum.Three)] + [TestCase("three", TestEnum.Three)] + [TestCase("Four", TestEnum.Four)] + [TestCase("four", TestEnum.Four)] + [TestCase("Four1", null)] + [TestCase(null, null)] + public void TestEnumConverterNullableDeserializeTests(string? value, TestEnum? expected) + { + var val = value == null ? "null" : $"\"{value}\""; + var output = JsonConvert.DeserializeObject<EnumObject?>($"{{ \"Value\": {val} }}"); + Assert.AreEqual(output.Value, expected); + } } public class TimeObject @@ -58,4 +97,21 @@ namespace CryptoExchange.Net.UnitTests [JsonConverter(typeof(DateTimeConverter))] public DateTime? Time { get; set; } } + + public class EnumObject + { + public TestEnum? Value { get; set; } + } + + [JsonConverter(typeof(EnumConverter))] + public enum TestEnum + { + [Map("1")] + One, + [Map("2")] + Two, + [Map("three", "3")] + Three, + Four + } } diff --git a/CryptoExchange.Net/Attributes/MapAttribute.cs b/CryptoExchange.Net/Attributes/MapAttribute.cs new file mode 100644 index 0000000..27f553a --- /dev/null +++ b/CryptoExchange.Net/Attributes/MapAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Attributes +{ + public class MapAttribute : Attribute + { + public string[] Values { get; set; } + public MapAttribute(params string[] maps) + { + Values = maps; + } + } +} diff --git a/CryptoExchange.Net/Converters/EnumConverter.cs b/CryptoExchange.Net/Converters/EnumConverter.cs new file mode 100644 index 0000000..9a90c55 --- /dev/null +++ b/CryptoExchange.Net/Converters/EnumConverter.cs @@ -0,0 +1,101 @@ +using CryptoExchange.Net.Attributes; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace CryptoExchange.Net.Converters +{ + public class EnumConverter : JsonConverter + { + private static ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new ConcurrentDictionary<Type, List<KeyValuePair<object, string>>>(); + + public override bool CanConvert(Type objectType) + { + return objectType.IsEnum; + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + if (!_mapping.TryGetValue(objectType, out var mapping)) + mapping = AddMapping(objectType); + + if (reader.Value == null) + return null; + + var stringValue = reader.Value.ToString(); + if (string.IsNullOrWhiteSpace(stringValue)) + return null; + + if (!GetValue(objectType, mapping, stringValue, out var result)) + { + Debug.WriteLine($"Cannot map enum. Type: {objectType.Name}, Value: {reader.Value}"); + return null; + } + + return result; + } + + private static List<KeyValuePair<object, string>> AddMapping(Type objectType) + { + var mapping = new List<KeyValuePair<object, string>>(); + var enumMembers = objectType.GetMembers(); + foreach (var member in enumMembers) + { + var maps = member.GetCustomAttributes(typeof(MapAttribute), false); + foreach (MapAttribute attribute in maps) + { + foreach (var value in attribute.Values) + mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value)); + } + } + _mapping.TryAdd(objectType, mapping); + return mapping; + } + + private bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result) + { + // Check for exact match first, then if not found fallback to a case insensitive match + var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair<object, string>))) + mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + + if (!mapping.Equals(default(KeyValuePair<object, string>))) + { + result = mapping.Key; + return true; + } + + try + { + result = Enum.Parse(objectType, value, true); + return true; + } + catch (Exception) + { + result = default; + return false; + } + } + + public static string? GetString<T>(T enumValue) + { + var objectType = typeof(T); + objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + + if (!_mapping.TryGetValue(objectType, out var mapping)) + mapping = AddMapping(objectType); + + return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + var stringValue = GetString(value); + writer.WriteRawValue(stringValue); + } + } +}