From 84b55fe0057ca2df9da590901357043506bcf32a Mon Sep 17 00:00:00 2001 From: Daniel Cronqvist Date: Thu, 5 Sep 2024 22:25:13 +0200 Subject: [PATCH] Added some new FromClass methods to construct custom class definitions, tests not done --- src/DotTiled.Tests/Assert/AssertMap.cs | 8 +- .../CustomTypes/CustomClassDefinitionTests.cs | 112 ++++++++++++++++++ .../CustomTypes/CustomClassDefinition.cs | 94 +++++++++++++++ 3 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 src/DotTiled.Tests/Properties/CustomTypes/CustomClassDefinitionTests.cs diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index c57a233..bc87002 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -5,7 +5,7 @@ namespace DotTiled.Tests; public static partial class DotTiledAssert { - private static void AssertListOrdered(IList expected, IList actual, string nameof, Action assertEqual = null) + internal static void AssertListOrdered(IList expected, IList actual, string nameof, Action assertEqual = null) { if (expected is null) { @@ -27,7 +27,7 @@ public static partial class DotTiledAssert } } - private static void AssertOptionalsEqual( + internal static void AssertOptionalsEqual( Optional expected, Optional actual, string nameof, @@ -49,7 +49,7 @@ public static partial class DotTiledAssert Assert.False(actual.HasValue, $"Expected {nameof} to not have a value"); } - private static void AssertEqual(Optional expected, Optional actual, string nameof) + internal static void AssertEqual(Optional expected, Optional actual, string nameof) { if (expected is null) { @@ -67,7 +67,7 @@ public static partial class DotTiledAssert Assert.False(actual.HasValue, $"Expected {nameof} to not have a value"); } - private static void AssertEqual(T expected, T actual, string nameof) + internal static void AssertEqual(T expected, T actual, string nameof) { if (expected == null) { diff --git a/src/DotTiled.Tests/Properties/CustomTypes/CustomClassDefinitionTests.cs b/src/DotTiled.Tests/Properties/CustomTypes/CustomClassDefinitionTests.cs new file mode 100644 index 0000000..af944f0 --- /dev/null +++ b/src/DotTiled.Tests/Properties/CustomTypes/CustomClassDefinitionTests.cs @@ -0,0 +1,112 @@ +namespace DotTiled.Tests; + +public class CustomClassDefinitionTests +{ + [Fact] + public void FromClassType_WhenTypeIsNotCustomClass_ThrowsArgumentException() + { + // Arrange + var type = typeof(string); + + // Act & Assert + Assert.Throws(() => CustomClassDefinition.FromClassType(type)); + } + + private sealed class TestClass1 + { + public string Name { get; set; } = "John Doe"; + public int Age { get; set; } = 42; + } + + private static CustomClassDefinition ExpectedTestClass1Definition => new CustomClassDefinition + { + Name = "TestClass1", + UseAs = CustomClassUseAs.All, + Members = new List + { + new StringProperty { Name = "Name", Value = "John Doe" }, + new IntProperty { Name = "Age", Value = 42 } + } + }; + + private sealed class TestClass2WithNestedClass + { + public string Name { get; set; } = "John Doe"; + public int Age { get; set; } = 42; + public TestClass1 Nested { get; set; } = new TestClass1(); + } + + private static CustomClassDefinition ExpectedTestClass2WithNestedClassDefinition => new CustomClassDefinition + { + Name = "TestClass2WithNestedClass", + UseAs = CustomClassUseAs.All, + Members = [ + new StringProperty { Name = "Name", Value = "John Doe" }, + new IntProperty { Name = "Age", Value = 42 }, + new ClassProperty + { + Name = "Nested", + PropertyType = "TestClass1", + Value = [] + } + ] + }; + + private sealed class TestClass3WithOverridenNestedClass + { + public string Name { get; set; } = "John Doe"; + public int Age { get; set; } = 42; + public TestClass1 Nested { get; set; } = new TestClass1 + { + Name = "Jane Doe" + }; + } + + private static CustomClassDefinition ExpectedTestClass3WithOverridenNestedClassDefinition => new CustomClassDefinition + { + Name = "TestClass3WithOverridenNestedClass", + UseAs = CustomClassUseAs.All, + Members = [ + new StringProperty { Name = "Name", Value = "John Doe" }, + new IntProperty { Name = "Age", Value = 42 }, + new ClassProperty + { + Name = "Nested", + PropertyType = "TestClass1", + Value = [ + new StringProperty { Name = "Name", Value = "Jane Doe" }, + ] + } + ] + }; + + private static IEnumerable<(Type, CustomClassDefinition)> GetCustomClassDefinitionTestData() + { + yield return (typeof(TestClass1), ExpectedTestClass1Definition); + yield return (typeof(TestClass2WithNestedClass), ExpectedTestClass2WithNestedClassDefinition); + yield return (typeof(TestClass3WithOverridenNestedClass), ExpectedTestClass3WithOverridenNestedClassDefinition); + } + + private static void AssertCustomClassDefinitionEqual(CustomClassDefinition expected, CustomClassDefinition actual) + { + DotTiledAssert.AssertEqual(expected.ID, actual.ID, nameof(CustomClassDefinition.ID)); + DotTiledAssert.AssertEqual(expected.Name, actual.Name, nameof(CustomClassDefinition.Name)); + DotTiledAssert.AssertEqual(expected.Color, actual.Color, nameof(CustomClassDefinition.Color)); + DotTiledAssert.AssertEqual(expected.DrawFill, actual.DrawFill, nameof(CustomClassDefinition.DrawFill)); + DotTiledAssert.AssertEqual(expected.UseAs, actual.UseAs, nameof(CustomClassDefinition.UseAs)); + DotTiledAssert.AssertProperties(expected.Members, actual.Members); + } + + public static IEnumerable CustomClassDefinitionTestData => + GetCustomClassDefinitionTestData().Select(data => new object[] { data.Item1, data.Item2 }); + [Theory] + [MemberData(nameof(CustomClassDefinitionTestData))] + public void FromClassType_WhenTypeIsCustomClass_ReturnsCustomClassDefinition(Type type, CustomClassDefinition expected) + { + // Arrange & Act + var result = CustomClassDefinition.FromClassType(type); + + // Assert + AssertCustomClassDefinitionEqual(expected, result); + } +} diff --git a/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs b/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs index 6a99f62..2c3be71 100644 --- a/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs +++ b/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; namespace DotTiled; @@ -95,4 +97,96 @@ public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition /// public override IList GetProperties() => Members; + + /// + /// Creates a new from the specified class type. + /// + /// The type of the class to create a custom class definition from. + /// A new instance. + /// Thrown when the specified type is not a class. + public static CustomClassDefinition FromClassType(Type type) + { + if (type == typeof(string) || !type.IsClass) + throw new ArgumentException("Type must be a class.", nameof(type)); + + return FromClass(() => Activator.CreateInstance(type)); + } + + /// + /// Creates a new from the specified instance of a class. + /// + /// The instance of the class to create a custom class definition from. + /// A new instance. + public static CustomClassDefinition FromClassInstance(dynamic instance) + { + ArgumentNullException.ThrowIfNull(instance); + return FromClass(() => instance); + } + + /// + /// Creates a new from the specified constructible class type. + /// + /// The type of the class to create a custom class definition from. + /// A new instance. + public static CustomClassDefinition FromClass() where T : class, new() => FromClass(() => new T()); + + /// + /// Creates a new from the specified factory function of a class instance. + /// + /// The type of the class to create a custom class definition from. + /// The factory function that creates an instance of the class. + /// A new instance. + public static CustomClassDefinition FromClass(Func factory) where T : class + { + var instance = factory(); + var type = instance.GetType(); + var properties = type.GetProperties(); + + return new CustomClassDefinition + { + Name = type.Name, + UseAs = CustomClassUseAs.All, + Members = properties.Select(p => ConvertPropertyInfoToIProperty(instance, p)).ToList() + }; + } + + private static IProperty ConvertPropertyInfoToIProperty(object instance, PropertyInfo propertyInfo) + { + switch (propertyInfo.PropertyType) + { + case Type t when t == typeof(bool): + return new BoolProperty { Name = propertyInfo.Name, Value = (bool)propertyInfo.GetValue(instance) }; + case Type t when t == typeof(Color): + return new ColorProperty { Name = propertyInfo.Name, Value = (Color)propertyInfo.GetValue(instance) }; + case Type t when t == typeof(float): + return new FloatProperty { Name = propertyInfo.Name, Value = (float)propertyInfo.GetValue(instance) }; + case Type t when t == typeof(string): + return new StringProperty { Name = propertyInfo.Name, Value = (string)propertyInfo.GetValue(instance) }; + case Type t when t == typeof(int): + return new IntProperty { Name = propertyInfo.Name, Value = (int)propertyInfo.GetValue(instance) }; + case Type t when t.IsClass: + return new ClassProperty { Name = propertyInfo.Name, PropertyType = t.Name, Value = GetNestedProperties(propertyInfo.PropertyType, propertyInfo.GetValue(instance)) }; + default: + break; + } + + throw new NotSupportedException($"Type '{propertyInfo.PropertyType.Name}' is not supported in custom classes."); + } + + private static List GetNestedProperties(Type type, object instance) + { + var defaultInstance = Activator.CreateInstance(type); + var properties = type.GetProperties(); + + bool IsPropertyDefaultValue(PropertyInfo propertyInfo) + { + var defaultValue = propertyInfo.GetValue(defaultInstance); + var value = propertyInfo.GetValue(instance); + return value.Equals(defaultValue); + } + + return properties + .Where(p => !IsPropertyDefaultValue(p)) + .Select(p => ConvertPropertyInfoToIProperty(instance, p)).ToList(); + } }