From 4393f6345628a32aacc6381ab17afe8a5c3df237 Mon Sep 17 00:00:00 2001 From: Sprix <91488389+Sprixitite@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:00:05 +0100 Subject: [PATCH] Implement deserialization of dictionaries using primitives as keys (#44) --- Tomlet.Tests/DictionaryTests.cs | 39 ++++++++++++++++++++++- Tomlet/Extensions/ReflectionExtensions.cs | 17 ++++++++++ Tomlet/TomlSerializationMethods.cs | 28 +++++++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/Tomlet.Tests/DictionaryTests.cs b/Tomlet.Tests/DictionaryTests.cs index 71df7cd..567ff4e 100644 --- a/Tomlet.Tests/DictionaryTests.cs +++ b/Tomlet.Tests/DictionaryTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Tomlet.Models; using Tomlet.Tests.TestModelClasses; using Xunit; @@ -53,4 +54,40 @@ public void DictionaryKeysShouldBeProperlyEscaped() Assert.Contains(expectedValue, serialized); } } + + private bool PrimitiveKeyTestHelper(params T[] values) where T : unmanaged, IConvertible + { + var primitiveDict = new Dictionary(); + for (int i=0; i>(serialized); + + foreach (var (key, value) in primitiveDict) { + if (!deserialized.ContainsKey(key)) { + return false; + } + if (deserialized[key] != value) { + return false; + } + } + return true; + } + + [Fact] + public void PrimitiveDictionaryKeysShouldWork() + { + Assert.True(PrimitiveKeyTestHelper(true, false)); + Assert.True(PrimitiveKeyTestHelper(long.MaxValue, long.MinValue, 0, 4736251)); + Assert.True(PrimitiveKeyTestHelper(uint.MinValue, uint.MaxValue, 0u, 1996u)); + + // \n causes an exception when deserializing + // I don't consider this a bug with the primitive dict deserializer because the string dict deserializer also has this issue + Assert.True(PrimitiveKeyTestHelper('a', 'b', 'c' /*, '\n' */)); + } + } \ No newline at end of file diff --git a/Tomlet/Extensions/ReflectionExtensions.cs b/Tomlet/Extensions/ReflectionExtensions.cs index bef5c61..ec316d9 100644 --- a/Tomlet/Extensions/ReflectionExtensions.cs +++ b/Tomlet/Extensions/ReflectionExtensions.cs @@ -32,5 +32,22 @@ internal static bool TryGetBestMatchConstructor(this Type type, out ConstructorI bestMatchConstructor = parameterizedConstructors.Single(); return true; } + + internal static bool IsIntegerType(this Type type) { + switch (Type.GetTypeCode(type)) { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.Int16: + case TypeCode.UInt32: + case TypeCode.Int32: + case TypeCode.UInt64: + case TypeCode.Int64: + return true; + default: + return false; + } + } + } } \ No newline at end of file diff --git a/Tomlet/TomlSerializationMethods.cs b/Tomlet/TomlSerializationMethods.cs index f274071..7b58ad7 100644 --- a/Tomlet/TomlSerializationMethods.cs +++ b/Tomlet/TomlSerializationMethods.cs @@ -1,10 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Tomlet.Attributes; using Tomlet.Exceptions; +using Tomlet.Extensions; using Tomlet.Models; namespace Tomlet @@ -12,6 +14,7 @@ namespace Tomlet public static class TomlSerializationMethods { private static MethodInfo _stringKeyedDictionaryMethod = typeof(TomlSerializationMethods).GetMethod(nameof(StringKeyedDictionaryDeserializerFor), BindingFlags.Static | BindingFlags.NonPublic)!; + private static MethodInfo _primitiveKeyedDictionaryMethod = typeof(TomlSerializationMethods).GetMethod(nameof(PrimitiveKeyedDictionaryDeserializerFor), BindingFlags.Static | BindingFlags.NonPublic)!; private static MethodInfo _genericDictionarySerializerMethod = typeof(TomlSerializationMethods).GetMethod(nameof(GenericDictionarySerializer), BindingFlags.Static | BindingFlags.NonPublic)!; private static MethodInfo _genericNullableSerializerMethod = typeof(TomlSerializationMethods).GetMethod(nameof(GenericNullableSerializer), BindingFlags.Static | BindingFlags.NonPublic)!; @@ -195,6 +198,12 @@ internal static Deserialize GetDeserializer(Type t, TomlSerializerOption { return (Deserialize)_stringKeyedDictionaryMethod.MakeGenericMethod(genericArgs[1]).Invoke(null, new object[]{options})!; } + + if (genericArgs[0].IsIntegerType() || genericArgs[0] == typeof(bool) || genericArgs[0] == typeof(char)) + { + // float primitives not supported due to decimal point causing issues + return (Deserialize)_primitiveKeyedDictionaryMethod.MakeGenericMethod(genericArgs).Invoke(null, new object[]{options})!; + } } return TomlCompositeDeserializer.For(t, options); @@ -279,7 +288,24 @@ private static Deserialize> StringKeyedDictionaryDeseriali return table.Entries.ToDictionary(entry => entry.Key, entry => (T)deserializer(entry.Value)); }; } - + + // unmanaged + IConvertible is the closest I can get to expressing "primitives only" + private static Deserialize> PrimitiveKeyedDictionaryDeserializerFor(TomlSerializerOptions options) where TKey : unmanaged, IConvertible + { + var valueDeserializer = GetDeserializer(typeof(TValue), options); + + return value => + { + if (value is not TomlTable table) + throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), typeof(Dictionary)); + + return table.Entries.ToDictionary( + entry => (TKey)(entry.Key as IConvertible).ToType(typeof(TKey), CultureInfo.InvariantCulture), + entry => (TValue)valueDeserializer(entry.Value) + ); + }; + } + private static TomlValue? GenericNullableSerializer(T? nullable, TomlSerializerOptions options) where T : struct { var elementSerializer = GetSerializer(typeof(T), options);