From 811b3233caf4e1c2c47a1179f9730d7ea5124e20 Mon Sep 17 00:00:00 2001 From: Tarcisio <60912483+tcortega@users.noreply.github.com> Date: Sun, 16 Jul 2023 14:03:22 -0300 Subject: [PATCH] Add better constructor support for deserialization (#32) --- Tomlet.Tests/ClassDeserializationTests.cs | 26 ++++++ .../ClassWithValuesSetOnConstructor.cs | 11 +++ Tomlet.Tests/ExceptionTests.cs | 8 ++ Tomlet.Tests/ObjectToStringTests.cs | 27 ++---- Tomlet.Tests/ObjectToTomlDocTests.cs | 13 +-- .../TestModelClasses/AbstractClass.cs | 6 ++ ...ssWithMultipleParameterizedConstructors.cs | 16 ++++ .../ClassWithParameterlessConstructor.cs | 9 ++ .../TestModelClasses/SimpleTestRecord.cs | 13 +-- .../Exceptions/TomlInstantiationException.cs | 14 +-- .../TomlParameterTypeMismatchException.cs | 19 ++++ .../GenericExtensions.cs} | 4 +- Tomlet/Extensions/ReflectionExtensions.cs | 36 ++++++++ Tomlet/Extensions/StringExtensions.cs | 24 +++++ Tomlet/Models/TomlString.cs | 1 + Tomlet/Models/TomlTable.cs | 3 +- Tomlet/TomlCompositeDeserializer.cs | 89 +++++++++++++------ Tomlet/TomlCompositeSerializer.cs | 21 ++--- Tomlet/TomlDateTimeUtils.cs | 1 + Tomlet/TomlNumberUtils.cs | 1 + Tomlet/TomlParser.cs | 1 + Tomlet/TomlSerializationMethods.cs | 60 ++++++------- Tomlet/TomlSerializerOptions.cs | 8 ++ Tomlet/TomletMain.cs | 35 ++++---- 24 files changed, 308 insertions(+), 138 deletions(-) create mode 100644 Tomlet.Tests/ClassWithValuesSetOnConstructor.cs create mode 100644 Tomlet.Tests/TestModelClasses/AbstractClass.cs create mode 100644 Tomlet.Tests/TestModelClasses/ClassWithMultipleParameterizedConstructors.cs create mode 100644 Tomlet.Tests/TestModelClasses/ClassWithParameterlessConstructor.cs create mode 100644 Tomlet/Exceptions/TomlParameterTypeMismatchException.cs rename Tomlet/{Extensions.cs => Extensions/GenericExtensions.cs} (98%) create mode 100644 Tomlet/Extensions/ReflectionExtensions.cs create mode 100644 Tomlet/Extensions/StringExtensions.cs create mode 100644 Tomlet/TomlSerializerOptions.cs diff --git a/Tomlet.Tests/ClassDeserializationTests.cs b/Tomlet.Tests/ClassDeserializationTests.cs index 8907021..4c2218a 100644 --- a/Tomlet.Tests/ClassDeserializationTests.cs +++ b/Tomlet.Tests/ClassDeserializationTests.cs @@ -54,6 +54,14 @@ public void SimpleRecordDeserializationWorks() Assert.Equal(new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc), type.MyDateTime); } + [Fact] + public void ClassWithParameterlessConstructorDeserializationWorks() + { + var type = TomletMain.To(TestResources.SimplePrimitiveDeserializationTestInput); + + Assert.Equal("Hello, world!", type.MyString); + } + [Fact] public void AnArrayOfEmptyStringsCanBeDeserialized() { @@ -75,5 +83,23 @@ public void AttemptingToDeserializeADocumentWithAnIncorrectlyTypedFieldThrows() var msg = $"While deserializing an object of type {typeof(SimplePrimitiveTestClass).FullName}, found field MyFloat expecting a type of Double, but value in TOML was of type String"; Assert.Equal(msg, ex.Message); } + + [Fact] + public void ShouldOverrideDefaultConstructorsValues() + { + var options = new TomlSerializerOptions { OverrideConstructorValues = true }; + var type = TomletMain.To(TestResources.SimplePrimitiveDeserializationTestInput, options); + + Assert.Equal("Hello, world!", type.MyString); + } + + [Fact] + public void ShouldNotOverrideDefaultConstructorsValues() + { + var options = new TomlSerializerOptions { OverrideConstructorValues = false }; + var type = TomletMain.To(TestResources.SimplePrimitiveDeserializationTestInput, options); + + Assert.Equal("Modified on constructor!", type.MyString); + } } } \ No newline at end of file diff --git a/Tomlet.Tests/ClassWithValuesSetOnConstructor.cs b/Tomlet.Tests/ClassWithValuesSetOnConstructor.cs new file mode 100644 index 0000000..1c94a5a --- /dev/null +++ b/Tomlet.Tests/ClassWithValuesSetOnConstructor.cs @@ -0,0 +1,11 @@ +namespace Tomlet.Tests; + +public class ClassWithValuesSetOnConstructor +{ + public ClassWithValuesSetOnConstructor(string myString) + { + MyString = "Modified on constructor!"; + } + + public string MyString { get; set; } +} \ No newline at end of file diff --git a/Tomlet.Tests/ExceptionTests.cs b/Tomlet.Tests/ExceptionTests.cs index 13d9349..8f9f55f 100644 --- a/Tomlet.Tests/ExceptionTests.cs +++ b/Tomlet.Tests/ExceptionTests.cs @@ -190,6 +190,14 @@ public void UnicodeControlCharsThrowAnException() => [Fact] public void UnInstantiableObjectsThrow() => AssertThrows(() => TomletMain.To("")); + + [Fact] + public void MultipleParameterizedConstructorsThrow() => + AssertThrows(() => TomletMain.To("")); + + [Fact] + public void AbstractClassDeserializationThrows() => + AssertThrows(() => TomletMain.To("")); [Fact] public void MismatchingTypesInPrimitiveMappingThrows() => diff --git a/Tomlet.Tests/ObjectToStringTests.cs b/Tomlet.Tests/ObjectToStringTests.cs index f0fd85b..a7f4939 100644 --- a/Tomlet.Tests/ObjectToStringTests.cs +++ b/Tomlet.Tests/ObjectToStringTests.cs @@ -77,34 +77,23 @@ public void SerializingSimplePropertyClassAndDeserializingAgainGivesEquivalentOb [Fact] public void SerializingSimpleTestRecordToTomlStringWorks() { - var testObject = new SimpleTestRecord - { - MyBool = true, - MyFloat = 420.69f, - MyString = "Hello, world!", - MyDateTime = new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc) - }; - + var testObject = new SimpleTestRecord("Hello, world!", 420.69f, true, + new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc)); var serializedForm = TomletMain.TomlStringFrom(testObject); - + Assert.Equal("MyString = \"Hello, world!\"\nMyFloat = 420.69000244140625\nMyBool = true\nMyDateTime = 1970-01-01T07:00:00", serializedForm.Trim()); } [Fact] public void SerializingSimpleTestRecordAndDeserializingAgainGivesEquivalentObject() { - var testObject = new SimpleTestRecord - { - MyBool = true, - MyFloat = 420.69f, - MyString = "Hello, world!", - MyDateTime = new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc) - }; + var testObject = new SimpleTestRecord("Hello, world!", 420.69f, true, + new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc)); var serializedForm = TomletMain.TomlStringFrom(testObject); - + var deserializedAgain = TomletMain.To(serializedForm); - + Assert.Equal(testObject, deserializedAgain); } @@ -120,7 +109,7 @@ public void SerializingAnEmptyObjectGivesAnEmptyString() public void AttemptingToDirectlySerializeNullThrows() { //We need to use a type of T that actually has something to serialize - Assert.Throws(() => TomletMain.DocumentFrom(typeof(SimplePrimitiveTestClass), null!)); + Assert.Throws(() => TomletMain.DocumentFrom(typeof(SimplePrimitiveTestClass), null!, null)); } } } \ No newline at end of file diff --git a/Tomlet.Tests/ObjectToTomlDocTests.cs b/Tomlet.Tests/ObjectToTomlDocTests.cs index f0efb42..dc9dd70 100644 --- a/Tomlet.Tests/ObjectToTomlDocTests.cs +++ b/Tomlet.Tests/ObjectToTomlDocTests.cs @@ -66,20 +66,15 @@ public void SimplePropertyClassToTomlDocWorks() Assert.Equal("Hello, world!", tomlDoc.GetString("MyString")); Assert.Equal("1970-01-01T07:00:00", tomlDoc.GetValue("MyDateTime").StringValue); } - + [Fact] public void SimpleTestRecordToTomlDocWorks() { - var testObject = new SimpleTestRecord - { - MyBool = true, - MyFloat = 420.69f, - MyString = "Hello, world!", - MyDateTime = new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc) - }; + var testObject = new SimpleTestRecord("Hello, world!", 420.69f, true, + new DateTime(1970, 1, 1, 7, 0, 0, DateTimeKind.Utc)); var tomlDoc = TomletMain.DocumentFrom(testObject); - + Assert.Equal(4, tomlDoc.Entries.Count); Assert.True(tomlDoc.GetBoolean("MyBool")); Assert.True(Math.Abs(tomlDoc.GetFloat("MyFloat") - 420.69) < 0.01); diff --git a/Tomlet.Tests/TestModelClasses/AbstractClass.cs b/Tomlet.Tests/TestModelClasses/AbstractClass.cs new file mode 100644 index 0000000..da37962 --- /dev/null +++ b/Tomlet.Tests/TestModelClasses/AbstractClass.cs @@ -0,0 +1,6 @@ +namespace Tomlet.Tests.TestModelClasses; + +public abstract class AbstractClass +{ + +} \ No newline at end of file diff --git a/Tomlet.Tests/TestModelClasses/ClassWithMultipleParameterizedConstructors.cs b/Tomlet.Tests/TestModelClasses/ClassWithMultipleParameterizedConstructors.cs new file mode 100644 index 0000000..87580a3 --- /dev/null +++ b/Tomlet.Tests/TestModelClasses/ClassWithMultipleParameterizedConstructors.cs @@ -0,0 +1,16 @@ +namespace Tomlet.Tests.TestModelClasses; + +public class ClassWithMultipleParameterizedConstructors +{ + public ClassWithMultipleParameterizedConstructors(string myString) + { + MyString = myString; + } + + public ClassWithMultipleParameterizedConstructors(string myString, int age) + { + MyString = myString; + } + + public string MyString { get; set; } +} \ No newline at end of file diff --git a/Tomlet.Tests/TestModelClasses/ClassWithParameterlessConstructor.cs b/Tomlet.Tests/TestModelClasses/ClassWithParameterlessConstructor.cs new file mode 100644 index 0000000..8bab1a0 --- /dev/null +++ b/Tomlet.Tests/TestModelClasses/ClassWithParameterlessConstructor.cs @@ -0,0 +1,9 @@ +namespace Tomlet.Tests.TestModelClasses; + +public class ClassWithParameterlessConstructor : ClassWithMultipleParameterizedConstructors +{ + public ClassWithParameterlessConstructor() : base(string.Empty, int.MinValue) + { + + } +} \ No newline at end of file diff --git a/Tomlet.Tests/TestModelClasses/SimpleTestRecord.cs b/Tomlet.Tests/TestModelClasses/SimpleTestRecord.cs index ef2098d..b0a7e5e 100644 --- a/Tomlet.Tests/TestModelClasses/SimpleTestRecord.cs +++ b/Tomlet.Tests/TestModelClasses/SimpleTestRecord.cs @@ -1,12 +1,5 @@ using System; -namespace Tomlet.Tests.TestModelClasses -{ - public record SimpleTestRecord - { - public string MyString { get; init; } - public float MyFloat { get; init; } - public bool MyBool { get; init; } - public DateTime MyDateTime { get; init; } - } -} +namespace Tomlet.Tests.TestModelClasses; + +public record SimpleTestRecord(string MyString, float MyFloat, bool MyBool, DateTime MyDateTime); \ No newline at end of file diff --git a/Tomlet/Exceptions/TomlInstantiationException.cs b/Tomlet/Exceptions/TomlInstantiationException.cs index b945ff4..ceef77f 100644 --- a/Tomlet/Exceptions/TomlInstantiationException.cs +++ b/Tomlet/Exceptions/TomlInstantiationException.cs @@ -1,16 +1,8 @@ -using System; - -namespace Tomlet.Exceptions +namespace Tomlet.Exceptions { public class TomlInstantiationException : TomlException { - private readonly Type _type; - - public TomlInstantiationException(Type type) - { - _type = type; - } - - public override string Message => $"Could not find a no-argument constructor for type {_type.FullName}"; + public override string Message => + "Deserialization of types without a parameterless constructor or a singular parameterized constructor is not supported."; } } \ No newline at end of file diff --git a/Tomlet/Exceptions/TomlParameterTypeMismatchException.cs b/Tomlet/Exceptions/TomlParameterTypeMismatchException.cs new file mode 100644 index 0000000..e9ea62c --- /dev/null +++ b/Tomlet/Exceptions/TomlParameterTypeMismatchException.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +namespace Tomlet.Exceptions +{ + public class TomlParameterTypeMismatchException : TomlTypeMismatchException + { + private readonly Type _typeBeingInstantiated; + private readonly ParameterInfo _paramBeingDeserialized; + + public TomlParameterTypeMismatchException(Type typeBeingInstantiated, ParameterInfo paramBeingDeserialized, TomlTypeMismatchException cause) : base(cause.ExpectedType, cause.ActualType, paramBeingDeserialized.ParameterType) + { + _typeBeingInstantiated = typeBeingInstantiated; + _paramBeingDeserialized = paramBeingDeserialized; + } + + public override string Message => $"While deserializing an object of type {_typeBeingInstantiated}, found parameter {_paramBeingDeserialized.Name} expecting a type of {ExpectedTypeName}, but value in TOML was of type {ActualTypeName}"; + } +} \ No newline at end of file diff --git a/Tomlet/Extensions.cs b/Tomlet/Extensions/GenericExtensions.cs similarity index 98% rename from Tomlet/Extensions.cs rename to Tomlet/Extensions/GenericExtensions.cs index f533c64..e56ac53 100644 --- a/Tomlet/Extensions.cs +++ b/Tomlet/Extensions/GenericExtensions.cs @@ -6,9 +6,9 @@ using System.Text; using Tomlet.Exceptions; -namespace Tomlet +namespace Tomlet.Extensions { - internal static class Extensions + internal static class GenericExtensions { private static readonly HashSet IllegalChars = new() { diff --git a/Tomlet/Extensions/ReflectionExtensions.cs b/Tomlet/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..bef5c61 --- /dev/null +++ b/Tomlet/Extensions/ReflectionExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Tomlet.Extensions +{ + internal static class ReflectionExtensions + { + internal static bool TryGetBestMatchConstructor(this Type type, out ConstructorInfo? bestMatchConstructor) + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length == 0) + { + bestMatchConstructor = null; + return false; + } + + var parameterlessConstructor = constructors.FirstOrDefault(c => c.GetParameters().Length == 0); + if (parameterlessConstructor != null) + { + bestMatchConstructor = parameterlessConstructor; + return true; + } + + var parameterizedConstructors = constructors.Where(c => c.GetParameters().Length > 0).ToArray(); + if (parameterizedConstructors.Length > 1) + { + bestMatchConstructor = null; + return false; + } + + bestMatchConstructor = parameterizedConstructors.Single(); + return true; + } + } +} \ No newline at end of file diff --git a/Tomlet/Extensions/StringExtensions.cs b/Tomlet/Extensions/StringExtensions.cs new file mode 100644 index 0000000..0c174d1 --- /dev/null +++ b/Tomlet/Extensions/StringExtensions.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace Tomlet.Extensions +{ + internal static class StringExtensions + { + internal static string ToPascalCase(this string str) + { + var sb = new StringBuilder(str.Length); + + if (str.Length > 0) + { + sb.Append(char.ToUpper(str[0])); + } + + for (var i = 1; i < str.Length; i++) + { + sb.Append(char.IsWhiteSpace(str[i - 1]) ? char.ToUpper(str[i]) : str[i]); + } + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Tomlet/Models/TomlString.cs b/Tomlet/Models/TomlString.cs index 79370ad..c0f5d4c 100644 --- a/Tomlet/Models/TomlString.cs +++ b/Tomlet/Models/TomlString.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Tomlet.Extensions; namespace Tomlet.Models { diff --git a/Tomlet/Models/TomlTable.cs b/Tomlet/Models/TomlTable.cs index c8633b2..ad0da97 100644 --- a/Tomlet/Models/TomlTable.cs +++ b/Tomlet/Models/TomlTable.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Tomlet.Exceptions; +using Tomlet.Extensions; namespace Tomlet.Models { diff --git a/Tomlet/TomlCompositeDeserializer.cs b/Tomlet/TomlCompositeDeserializer.cs index 6a29700..5a6b138 100644 --- a/Tomlet/TomlCompositeDeserializer.cs +++ b/Tomlet/TomlCompositeDeserializer.cs @@ -5,18 +5,19 @@ using System.Runtime.CompilerServices; using Tomlet.Attributes; using Tomlet.Exceptions; +using Tomlet.Extensions; using Tomlet.Models; namespace Tomlet; internal static class TomlCompositeDeserializer { - public static TomlSerializationMethods.Deserialize For(Type type) + public static TomlSerializationMethods.Deserialize For(Type type, TomlSerializerOptions options) { TomlSerializationMethods.Deserialize deserializer; if (type.IsEnum) { - var stringDeserializer = TomlSerializationMethods.GetDeserializer(typeof(string)); + var stringDeserializer = TomlSerializationMethods.GetDeserializer(typeof(string), options); deserializer = value => { var enumName = (string)stringDeserializer.Invoke(value); @@ -37,53 +38,38 @@ public static TomlSerializationMethods.Deserialize For(Type type) var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); //Ignore NonSerialized and CompilerGenerated fields. - fields = fields.Where(f => !f.IsNotSerialized && f.GetCustomAttribute() == null).ToArray(); + fields = fields.Where(f => !f.IsNotSerialized && GenericExtensions.GetCustomAttribute(f) == null).ToArray(); var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); //Ignore TomlNonSerializedAttribute Decorated Properties var propsDict = props - .Where(p => p.GetSetMethod(true) != null && p.GetCustomAttribute() == null) - .Select(p => new KeyValuePair(p, p.GetCustomAttribute())) + .Where(p => p.GetSetMethod(true) != null && GenericExtensions.GetCustomAttribute(p) == null) + .Select(p => new KeyValuePair(p, GenericExtensions.GetCustomAttribute(p))) .ToDictionary(tuple => tuple.Key, tuple => tuple.Value); if (fields.Length + propsDict.Count == 0) - return _ => - { - try - { - return Activator.CreateInstance(type)!; - } - catch (MissingMethodException) - { - throw new TomlInstantiationException(type); - } - }; + return value => CreateInstance(type, value, options, out _); deserializer = value => { if (value is not TomlTable table) throw new TomlTypeMismatchException(typeof(TomlTable), value.GetType(), type); - object instance; - try - { - instance = Activator.CreateInstance(type)!; - } - catch (MissingMethodException) - { - throw new TomlInstantiationException(type); - } + var instance = CreateInstance(type, value, options, out var assignedMembers); foreach (var field in fields) { + if (!options.OverrideConstructorValues && assignedMembers.Contains(field.Name)) + continue; + if (!table.TryGetValue(field.Name, out var entry)) continue; //TODO: Do we want to make this configurable? As in, throw exception if data is missing? object fieldValue; try { - fieldValue = TomlSerializationMethods.GetDeserializer(field.FieldType).Invoke(entry!); + fieldValue = TomlSerializationMethods.GetDeserializer(field.FieldType, options).Invoke(entry!); } catch (TomlTypeMismatchException e) { @@ -96,6 +82,9 @@ public static TomlSerializationMethods.Deserialize For(Type type) foreach (var (prop, attribute) in propsDict) { var name = attribute?.GetMappedString() ?? prop.Name; + if (!options.OverrideConstructorValues && assignedMembers.Contains(name)) + continue; + if (!table.TryGetValue(name, out var entry)) continue; //TODO: As above, configurable? @@ -103,8 +92,9 @@ public static TomlSerializationMethods.Deserialize For(Type type) try { - propValue = TomlSerializationMethods.GetDeserializer(prop.PropertyType).Invoke(entry!); - } catch (TomlTypeMismatchException e) + propValue = TomlSerializationMethods.GetDeserializer(prop.PropertyType, options).Invoke(entry!); + } + catch (TomlTypeMismatchException e) { throw new TomlPropertyTypeMismatchException(type, prop, e); } @@ -121,4 +111,47 @@ public static TomlSerializationMethods.Deserialize For(Type type) return deserializer; } + + private static object CreateInstance(Type type, TomlValue tomlValue, TomlSerializerOptions options, out HashSet assignedMembers) + { + if (tomlValue is not TomlTable table) + throw new TomlTypeMismatchException(typeof(TomlTable), tomlValue.GetType(), type); + + if (!type.TryGetBestMatchConstructor(out var constructor)) + { + throw new TomlInstantiationException(); + } + + var parameters = constructor!.GetParameters(); + if (parameters.Length == 0) + { + assignedMembers = new HashSet(); + return constructor.Invoke(null); + } + + assignedMembers = new HashSet(StringComparer.OrdinalIgnoreCase); + var arguments = new object[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + object argument; + + if (!table.TryGetValue(parameter.Name!.ToPascalCase(), out var entry)) + continue; + + try + { + argument = TomlSerializationMethods.GetDeserializer(parameter.ParameterType, options).Invoke(entry!); + } + catch (TomlTypeMismatchException e) + { + throw new TomlParameterTypeMismatchException(parameter.ParameterType, parameter, e); + } + + arguments[i] = argument; + assignedMembers.Add(parameter.Name!); + } + + return constructor.Invoke(arguments); + } } \ No newline at end of file diff --git a/Tomlet/TomlCompositeSerializer.cs b/Tomlet/TomlCompositeSerializer.cs index 935303e..e73df1d 100644 --- a/Tomlet/TomlCompositeSerializer.cs +++ b/Tomlet/TomlCompositeSerializer.cs @@ -3,19 +3,20 @@ using System.Reflection; using System.Runtime.CompilerServices; using Tomlet.Attributes; +using Tomlet.Extensions; using Tomlet.Models; namespace Tomlet; internal static class TomlCompositeSerializer { - public static TomlSerializationMethods.Serialize For(Type type) + public static TomlSerializationMethods.Serialize For(Type type, TomlSerializerOptions options) { TomlSerializationMethods.Serialize serializer; if (type.IsEnum) { - var stringSerializer = TomlSerializationMethods.GetSerializer(typeof(string)); + var stringSerializer = TomlSerializationMethods.GetSerializer(typeof(string), options); serializer = o => stringSerializer.Invoke(Enum.GetName(type, o!) ?? throw new ArgumentException($"Tomlet: Cannot serialize {o} as an enum of type {type} because the enum type does not declare a name for that value")); } else @@ -23,21 +24,21 @@ public static TomlSerializationMethods.Serialize For(Type type) //Get all instance fields var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var fieldAttribs = fields - .ToDictionary(f => f, f => new {inline = f.GetCustomAttribute(), preceding = f.GetCustomAttribute(), noInline = f.GetCustomAttribute()}); + .ToDictionary(f => f, f => new {inline = GenericExtensions.GetCustomAttribute(f), preceding = GenericExtensions.GetCustomAttribute(f), noInline = GenericExtensions.GetCustomAttribute(f)}); var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .ToArray(); var propAttribs = props - .ToDictionary(p => p, p => new {inline = p.GetCustomAttribute(), preceding = p.GetCustomAttribute(), prop = p.GetCustomAttribute(), noInline = p.GetCustomAttribute()}); + .ToDictionary(p => p, p => new {inline = GenericExtensions.GetCustomAttribute(p), preceding = GenericExtensions.GetCustomAttribute(p), prop = GenericExtensions.GetCustomAttribute(p), noInline = GenericExtensions.GetCustomAttribute(p)}); - var isForcedNoInline = type.GetCustomAttribute() != null; + var isForcedNoInline = GenericExtensions.GetCustomAttribute(type) != null; //Ignore NonSerialized and CompilerGenerated fields. - fields = fields.Where(f => !(f.IsNotSerialized || f.GetCustomAttribute() != null) - && f.GetCustomAttribute() == null + fields = fields.Where(f => !(f.IsNotSerialized || GenericExtensions.GetCustomAttribute(f) != null) + && GenericExtensions.GetCustomAttribute(f) == null && !f.Name.Contains('<')).ToArray(); //Ignore TomlNonSerializedAttribute Decorated Properties - props = props.Where(p => p.GetCustomAttribute() == null).ToArray(); + props = props.Where(p => GenericExtensions.GetCustomAttribute(p) == null).ToArray(); if (fields.Length + props.Length == 0) return _ => new TomlTable(); @@ -56,7 +57,7 @@ public static TomlSerializationMethods.Serialize For(Type type) if (fieldValue == null) continue; //Skip nulls - TOML doesn't support them. - var tomlValue = TomlSerializationMethods.GetSerializer(field.FieldType).Invoke(fieldValue); + var tomlValue = TomlSerializationMethods.GetSerializer(field.FieldType, options).Invoke(fieldValue); if(tomlValue == null) continue; @@ -91,7 +92,7 @@ public static TomlSerializationMethods.Serialize For(Type type) if(propValue == null) continue; - var tomlValue = TomlSerializationMethods.GetSerializer(prop.PropertyType).Invoke(propValue); + var tomlValue = TomlSerializationMethods.GetSerializer(prop.PropertyType, options).Invoke(propValue); if (tomlValue == null) continue; diff --git a/Tomlet/TomlDateTimeUtils.cs b/Tomlet/TomlDateTimeUtils.cs index 04ad944..e636a64 100644 --- a/Tomlet/TomlDateTimeUtils.cs +++ b/Tomlet/TomlDateTimeUtils.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Tomlet.Exceptions; +using Tomlet.Extensions; using Tomlet.Models; namespace Tomlet diff --git a/Tomlet/TomlNumberUtils.cs b/Tomlet/TomlNumberUtils.cs index 4a95054..2141b02 100644 --- a/Tomlet/TomlNumberUtils.cs +++ b/Tomlet/TomlNumberUtils.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using Tomlet.Extensions; namespace Tomlet { diff --git a/Tomlet/TomlParser.cs b/Tomlet/TomlParser.cs index 915f8aa..4ec7dfe 100644 --- a/Tomlet/TomlParser.cs +++ b/Tomlet/TomlParser.cs @@ -6,6 +6,7 @@ using System.Text; using Tomlet.Attributes; using Tomlet.Exceptions; +using Tomlet.Extensions; using Tomlet.Models; namespace Tomlet diff --git a/Tomlet/TomlSerializationMethods.cs b/Tomlet/TomlSerializationMethods.cs index d68d928..1f4bdb5 100644 --- a/Tomlet/TomlSerializationMethods.cs +++ b/Tomlet/TomlSerializationMethods.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using Tomlet.Attributes; using Tomlet.Exceptions; using Tomlet.Models; @@ -17,8 +16,9 @@ public static class TomlSerializationMethods private static MethodInfo _genericNullableSerializerMethod = typeof(TomlSerializationMethods).GetMethod(nameof(GenericNullableSerializer), BindingFlags.Static | BindingFlags.NonPublic)!; public delegate T Deserialize(TomlValue value); - + public delegate T ComplexDeserialize(TomlValue value, TomlSerializerOptions options); public delegate TomlValue? Serialize(T? t); + public delegate TomlValue? ComplexSerialize(T? t, TomlSerializerOptions options); private static readonly Dictionary Deserializers = new(); private static readonly Dictionary Serializers = new(); @@ -75,8 +75,10 @@ static TomlSerializationMethods() Register(lt => new TomlLocalTime(lt), value => (value as TomlLocalTime)?.Value ?? throw new TomlTypeMismatchException(typeof(TomlLocalTime), value.GetType(), typeof(TimeSpan))); } - internal static Serialize GetSerializer(Type t) + internal static Serialize GetSerializer(Type t, TomlSerializerOptions? options) { + options ??= TomlSerializerOptions.Default; + if (Serializers.TryGetValue(t, out var value)) return (Serialize)value; @@ -93,8 +95,8 @@ internal static Serialize GetSerializer(Type t) { var serializer = _genericDictionarySerializerMethod.MakeGenericMethod(genericArgs); - var del = Delegate.CreateDelegate(typeof(Serialize<>).MakeGenericType(t), serializer); - var ret = (Serialize)(dict => (TomlValue?)del.DynamicInvoke(dict)); + var del = Delegate.CreateDelegate(typeof(ComplexSerialize<>).MakeGenericType(t), serializer); + var ret = (Serialize)(dict => (TomlValue?)del.DynamicInvoke(dict, options)); Serializers[t] = ret; return ret; @@ -104,39 +106,41 @@ internal static Serialize GetSerializer(Type t) { var serializer = _genericNullableSerializerMethod.MakeGenericMethod(genericArgs); - var del = Delegate.CreateDelegate(typeof(Serialize<>).MakeGenericType(t), serializer); - var ret = (Serialize)(dict => (TomlValue?)del.DynamicInvoke(dict)); + var del = Delegate.CreateDelegate(typeof(ComplexSerialize<>).MakeGenericType(t), serializer); + var ret = (Serialize)(dict => (TomlValue?)del.DynamicInvoke(dict, options)); Serializers[t] = ret; return ret; } } - return TomlCompositeSerializer.For(t); + return TomlCompositeSerializer.For(t, options); } - internal static Deserialize GetDeserializer(Type t) + internal static Deserialize GetDeserializer(Type t, TomlSerializerOptions? options) { + options ??= TomlSerializerOptions.Default; + if (Deserializers.TryGetValue(t, out var value)) return (Deserialize)value; if (t.IsArray) { - var arrayDeserializer = ArrayDeserializerFor(t.GetElementType()!); + var arrayDeserializer = ArrayDeserializerFor(t.GetElementType()!, options); Deserializers[t] = arrayDeserializer; return arrayDeserializer; } if (t.Namespace == "System.Collections.Generic" && t.Name == "List`1") { - var listDeserializer = ListDeserializerFor(t.GetGenericArguments()[0]); + var listDeserializer = ListDeserializerFor(t.GetGenericArguments()[0], options); Deserializers[t] = listDeserializer; return listDeserializer; } if(t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) && t.GetGenericArguments() is {Length: 1} genericArguments) { - var nullableDeserializer = NullableDeserializerFor(t); + var nullableDeserializer = NullableDeserializerFor(t, options); Deserializers[t] = nullableDeserializer; return nullableDeserializer; } @@ -145,15 +149,11 @@ internal static Deserialize GetDeserializer(Type t) { if (genericArgs[0] == typeof(string)) { -#if NETFRAMEWORK - return (Deserialize)_stringKeyedDictionaryMethod.MakeGenericMethod(genericArgs[1]).Invoke(null, new object[0])!; -#else - return (Deserialize)_stringKeyedDictionaryMethod.MakeGenericMethod(genericArgs[1]).Invoke(null, Array.Empty())!; -#endif + return (Deserialize)_stringKeyedDictionaryMethod.MakeGenericMethod(genericArgs[1]).Invoke(null, new object[]{options})!; } } - return TomlCompositeDeserializer.For(t); + return TomlCompositeDeserializer.For(t, options); } private static Serialize GenericEnumerableSerializer() => @@ -171,14 +171,14 @@ internal static Deserialize GetDeserializer(Type t) return ret; }; - private static Deserialize ArrayDeserializerFor(Type elementType) => + private static Deserialize ArrayDeserializerFor(Type elementType, TomlSerializerOptions options) => value => { if (value is not TomlArray tomlArray) throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), elementType.MakeArrayType()); var ret = Array.CreateInstance(elementType, tomlArray.Count); - var deserializer = GetDeserializer(elementType); + var deserializer = GetDeserializer(elementType, options); for (var index = 0; index < tomlArray.ArrayValues.Count; index++) { var arrayValue = tomlArray.ArrayValues[index]; @@ -188,7 +188,7 @@ private static Deserialize ArrayDeserializerFor(Type elementType) => return ret; }; - private static Deserialize ListDeserializerFor(Type elementType) + private static Deserialize ListDeserializerFor(Type elementType, TomlSerializerOptions options) { var listType = typeof(List<>).MakeGenericType(elementType); var relevantAddMethod = listType.GetMethod("Add")!; @@ -199,7 +199,7 @@ private static Deserialize ListDeserializerFor(Type elementType) throw new TomlTypeMismatchException(typeof(TomlArray), value.GetType(), listType); var ret = Activator.CreateInstance(listType)!; - var deserializer = GetDeserializer(elementType); + var deserializer = GetDeserializer(elementType, options); foreach (var arrayValue in tomlArray.ArrayValues) { @@ -210,10 +210,10 @@ private static Deserialize ListDeserializerFor(Type elementType) }; } - private static Deserialize NullableDeserializerFor(Type nullableType) + private static Deserialize NullableDeserializerFor(Type nullableType, TomlSerializerOptions options) { var elementType = nullableType.GetGenericArguments()[0]; - var elementDeserializer = GetDeserializer(elementType); + var elementDeserializer = GetDeserializer(elementType, options); return value => { @@ -223,9 +223,9 @@ private static Deserialize NullableDeserializerFor(Type nullableType) }; } - private static Deserialize> StringKeyedDictionaryDeserializerFor() + private static Deserialize> StringKeyedDictionaryDeserializerFor(TomlSerializerOptions options) { - var deserializer = GetDeserializer(typeof(T)); + var deserializer = GetDeserializer(typeof(T), options); return value => { @@ -236,9 +236,9 @@ private static Deserialize> StringKeyedDictionaryDeseriali }; } - private static TomlValue? GenericNullableSerializer(T? nullable) where T : struct + private static TomlValue? GenericNullableSerializer(T? nullable, TomlSerializerOptions options) where T : struct { - var elementSerializer = GetSerializer(typeof(T)); + var elementSerializer = GetSerializer(typeof(T), options); if (nullable.HasValue) return elementSerializer(nullable.Value); @@ -246,9 +246,9 @@ private static Deserialize> StringKeyedDictionaryDeseriali return null; } - private static TomlValue GenericDictionarySerializer(Dictionary dict) where TKey : notnull + private static TomlValue GenericDictionarySerializer(Dictionary dict, TomlSerializerOptions options) where TKey : notnull { - var valueSerializer = GetSerializer(typeof(TValue)); + var valueSerializer = GetSerializer(typeof(TValue), options); var ret = new TomlTable(); foreach (var entry in dict) diff --git a/Tomlet/TomlSerializerOptions.cs b/Tomlet/TomlSerializerOptions.cs new file mode 100644 index 0000000..5ced93b --- /dev/null +++ b/Tomlet/TomlSerializerOptions.cs @@ -0,0 +1,8 @@ +namespace Tomlet +{ + public class TomlSerializerOptions + { + public static TomlSerializerOptions Default = new(); + public bool OverrideConstructorValues { get; set; } = false; + } +} \ No newline at end of file diff --git a/Tomlet/TomletMain.cs b/Tomlet/TomletMain.cs index d3e6942..c391faa 100644 --- a/Tomlet/TomletMain.cs +++ b/Tomlet/TomletMain.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; -using Tomlet.Attributes; using Tomlet.Exceptions; using Tomlet.Models; @@ -15,22 +14,22 @@ public static class TomletMain public static void RegisterMapper(TomlSerializationMethods.Serialize? serializer, TomlSerializationMethods.Deserialize? deserializer) => TomlSerializationMethods.Register(serializer, deserializer); - public static T To(string tomlString) + public static T To(string tomlString, TomlSerializerOptions? options = null) { var parser = new TomlParser(); var tomlDocument = parser.Parse(tomlString); - return To(tomlDocument); + return To(tomlDocument, options); } - public static T To(TomlValue value) + public static T To(TomlValue value, TomlSerializerOptions? options = null) { - return (T)To(typeof(T), value); + return (T)To(typeof(T), value, options); } - public static object To(Type what, TomlValue value) + public static object To(Type what, TomlValue value, TomlSerializerOptions? options = null) { - var deserializer = TomlSerializationMethods.GetDeserializer(what); + var deserializer = TomlSerializationMethods.GetDeserializer(what, options); return deserializer.Invoke(value); } @@ -38,37 +37,37 @@ public static object To(Type what, TomlValue value) #if NET6_0 [return: NotNullIfNotNull("t")] #endif - public static TomlValue? ValueFrom(T t) + public static TomlValue? ValueFrom(T t, TomlSerializerOptions? options = null) { if (t == null) throw new ArgumentNullException(nameof(t)); - return ValueFrom(t.GetType(), t); + return ValueFrom(t.GetType(), t, options); } #if NET6_0 [return: NotNullIfNotNull("t")] #endif - public static TomlValue? ValueFrom(Type type, object t) + public static TomlValue? ValueFrom(Type type, object t, TomlSerializerOptions? options = null) { - var serializer = TomlSerializationMethods.GetSerializer(type); + var serializer = TomlSerializationMethods.GetSerializer(type, options); var tomlValue = serializer.Invoke(t); return tomlValue!; } - public static TomlDocument DocumentFrom(T t) + public static TomlDocument DocumentFrom(T t, TomlSerializerOptions? options = null) { if (t == null) throw new ArgumentNullException(nameof(t)); - return DocumentFrom(t.GetType(), t); + return DocumentFrom(t.GetType(), t, options); } - public static TomlDocument DocumentFrom(Type type, object t) + public static TomlDocument DocumentFrom(Type type, object t, TomlSerializerOptions? options = null) { - var val = ValueFrom(type, t); + var val = ValueFrom(type, t, options); return val switch { @@ -78,8 +77,8 @@ public static TomlDocument DocumentFrom(Type type, object t) }; } - public static string TomlStringFrom(T t) => DocumentFrom(t).SerializedValue; + public static string TomlStringFrom(T t, TomlSerializerOptions? options = null) => DocumentFrom(t, options).SerializedValue; - public static string TomlStringFrom(Type type, object t) => DocumentFrom(type, t).SerializedValue; + public static string TomlStringFrom(Type type, object t, TomlSerializerOptions? options = null) => DocumentFrom(type, t, options).SerializedValue; } } \ No newline at end of file