Skip to content

Commit

Permalink
Add better constructor support for deserialization (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
tcortega authored Jul 16, 2023
1 parent 729f57b commit 811b323
Show file tree
Hide file tree
Showing 24 changed files with 308 additions and 138 deletions.
26 changes: 26 additions & 0 deletions Tomlet.Tests/ClassDeserializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassWithParameterlessConstructor>(TestResources.SimplePrimitiveDeserializationTestInput);

Assert.Equal("Hello, world!", type.MyString);
}

[Fact]
public void AnArrayOfEmptyStringsCanBeDeserialized()
{
Expand All @@ -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<ClassWithValuesSetOnConstructor>(TestResources.SimplePrimitiveDeserializationTestInput, options);

Assert.Equal("Hello, world!", type.MyString);
}

[Fact]
public void ShouldNotOverrideDefaultConstructorsValues()
{
var options = new TomlSerializerOptions { OverrideConstructorValues = false };
var type = TomletMain.To<ClassWithValuesSetOnConstructor>(TestResources.SimplePrimitiveDeserializationTestInput, options);

Assert.Equal("Modified on constructor!", type.MyString);
}
}
}
11 changes: 11 additions & 0 deletions Tomlet.Tests/ClassWithValuesSetOnConstructor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Tomlet.Tests;

public class ClassWithValuesSetOnConstructor
{
public ClassWithValuesSetOnConstructor(string myString)
{
MyString = "Modified on constructor!";
}

public string MyString { get; set; }
}
8 changes: 8 additions & 0 deletions Tomlet.Tests/ExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ public void UnicodeControlCharsThrowAnException() =>
[Fact]
public void UnInstantiableObjectsThrow() =>
AssertThrows<TomlInstantiationException>(() => TomletMain.To<IConvertible>(""));

[Fact]
public void MultipleParameterizedConstructorsThrow() =>
AssertThrows<TomlInstantiationException>(() => TomletMain.To<ClassWithMultipleParameterizedConstructors>(""));

[Fact]
public void AbstractClassDeserializationThrows() =>
AssertThrows<TomlInstantiationException>(() => TomletMain.To<AbstractClass>(""));

[Fact]
public void MismatchingTypesInPrimitiveMappingThrows() =>
Expand Down
27 changes: 8 additions & 19 deletions Tomlet.Tests/ObjectToStringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SimpleTestRecord>(serializedForm);

Assert.Equal(testObject, deserializedAgain);
}

Expand All @@ -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<ArgumentNullException>(() => TomletMain.DocumentFrom(typeof(SimplePrimitiveTestClass), null!));
Assert.Throws<ArgumentNullException>(() => TomletMain.DocumentFrom(typeof(SimplePrimitiveTestClass), null!, null));
}
}
}
13 changes: 4 additions & 9 deletions Tomlet.Tests/ObjectToTomlDocTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions Tomlet.Tests/TestModelClasses/AbstractClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Tomlet.Tests.TestModelClasses;

public abstract class AbstractClass
{

}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Tomlet.Tests.TestModelClasses;

public class ClassWithParameterlessConstructor : ClassWithMultipleParameterizedConstructors
{
public ClassWithParameterlessConstructor() : base(string.Empty, int.MinValue)
{

}
}
13 changes: 3 additions & 10 deletions Tomlet.Tests/TestModelClasses/SimpleTestRecord.cs
Original file line number Diff line number Diff line change
@@ -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);
14 changes: 3 additions & 11 deletions Tomlet/Exceptions/TomlInstantiationException.cs
Original file line number Diff line number Diff line change
@@ -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.";
}
}
19 changes: 19 additions & 0 deletions Tomlet/Exceptions/TomlParameterTypeMismatchException.cs
Original file line number Diff line number Diff line change
@@ -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}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> IllegalChars = new()
{
Expand Down
36 changes: 36 additions & 0 deletions Tomlet/Extensions/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
24 changes: 24 additions & 0 deletions Tomlet/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
1 change: 1 addition & 0 deletions Tomlet/Models/TomlString.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Tomlet.Extensions;

namespace Tomlet.Models
{
Expand Down
3 changes: 2 additions & 1 deletion Tomlet/Models/TomlTable.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
Loading

0 comments on commit 811b323

Please sign in to comment.