diff --git a/src/DotTiled.Tests/Properties/HasPropertiesBaseTests.cs b/src/DotTiled.Tests/Properties/HasPropertiesBaseTests.cs new file mode 100644 index 0000000..4e53b57 --- /dev/null +++ b/src/DotTiled.Tests/Properties/HasPropertiesBaseTests.cs @@ -0,0 +1,194 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public class HasPropertiesBaseTests +{ + private sealed class TestHasProperties(IList props) : HasPropertiesBase + { + public override IList GetProperties() => props; + } + + private sealed class MapTo + { + public bool MapToBool { get; set; } = false; + public Color MapToColor { get; set; } = Color.Parse("#00000000", CultureInfo.InvariantCulture); + public float MapToFloat { get; set; } = 0.0f; + public string MapToFile { get; set; } = ""; + public int MapToInt { get; set; } = 0; + public int MapToObject { get; set; } = 0; + public string MapToString { get; set; } = ""; + } + + [Fact] + public void GetMappedProperty_PropertyNotFound_ThrowsKeyNotFoundException() + { + // Arrange + List props = [ + new ClassProperty { + Name = "ClassInObject", + PropertyType = "MapTo", + Value = [ + new StringProperty { Name = "PropertyThatDoesNotExistInMapTo", Value = "Test" } + ], + } + ]; + var hasProperties = new TestHasProperties(props); + + // Act + var act = () => hasProperties.GetMappedProperty("ClassInObject"); + + // Assert + Assert.Throws(act); + } + + [Fact] + public void GetMappedProperty_AllBasicValidProperties_ReturnsMappedProperty() + { + // Arrange + List props = [ + new ClassProperty { + Name = "ClassInObject", + PropertyType = "MapTo", + Value = [ + new BoolProperty { Name = "MapToBool", Value = true }, + new ColorProperty { Name = "MapToColor", Value = Color.Parse("#FF0000FF", CultureInfo.InvariantCulture) }, + new FloatProperty { Name = "MapToFloat", Value = 1.0f }, + new StringProperty { Name = "MapToFile", Value = "Test" }, + new IntProperty { Name = "MapToInt", Value = 1 }, + new IntProperty { Name = "MapToObject", Value = 1 }, + new StringProperty { Name = "MapToString", Value = "Test" }, + ], + } + ]; + var hasProperties = new TestHasProperties(props); + + // Act + var mappedProperty = hasProperties.GetMappedProperty("ClassInObject"); + + // Assert + Assert.True(mappedProperty.MapToBool); + Assert.Equal(Color.Parse("#FF0000FF", CultureInfo.InvariantCulture), mappedProperty.MapToColor); + Assert.Equal(1.0f, mappedProperty.MapToFloat); + Assert.Equal("Test", mappedProperty.MapToFile); + Assert.Equal(1, mappedProperty.MapToInt); + Assert.Equal(1, mappedProperty.MapToObject); + Assert.Equal("Test", mappedProperty.MapToString); + } + + private sealed class NestedMapTo + { + public string NestedMapToString { get; set; } = ""; + public MapTo MapToInNested { get; set; } = new MapTo(); + } + + [Fact] + public void GetMappedProperty_NestedMapTo_ReturnsMappedProperty() + { + // Arrange + List props = [ + new ClassProperty { + Name = "ClassInObject", + PropertyType = "NestedMapTo", + Value = [ + new StringProperty { Name = "NestedMapToString", Value = "Test" }, + new ClassProperty { + Name = "MapToInNested", + PropertyType = "MapTo", + Value = [ + new BoolProperty { Name = "MapToBool", Value = true }, + new ColorProperty { Name = "MapToColor", Value = Color.Parse("#FF0000FF", CultureInfo.InvariantCulture) }, + new FloatProperty { Name = "MapToFloat", Value = 1.0f }, + new StringProperty { Name = "MapToFile", Value = "Test" }, + new IntProperty { Name = "MapToInt", Value = 1 }, + new IntProperty { Name = "MapToObject", Value = 1 }, + new StringProperty { Name = "MapToString", Value = "Test" }, + ], + }, + ], + } + ]; + var hasProperties = new TestHasProperties(props); + + // Act + var mappedProperty = hasProperties.GetMappedProperty("ClassInObject"); + + // Assert + Assert.Equal("Test", mappedProperty.NestedMapToString); + Assert.True(mappedProperty.MapToInNested.MapToBool); + Assert.Equal(Color.Parse("#FF0000FF", CultureInfo.InvariantCulture), mappedProperty.MapToInNested.MapToColor); + Assert.Equal(1.0f, mappedProperty.MapToInNested.MapToFloat); + Assert.Equal("Test", mappedProperty.MapToInNested.MapToFile); + Assert.Equal(1, mappedProperty.MapToInNested.MapToInt); + Assert.Equal(1, mappedProperty.MapToInNested.MapToObject); + Assert.Equal("Test", mappedProperty.MapToInNested.MapToString); + } + + private enum TestEnum + { + TestValue1, + TestValue2, + TestValue3 + } + + private sealed class EnumMapTo + { + public TestEnum EnumMapToEnum { get; set; } = TestEnum.TestValue1; + } + + [Fact] + public void GetMappedProperty_EnumProperty_ReturnsMappedProperty() + { + // Arrange + List props = [ + new ClassProperty { + Name = "ClassInObject", + PropertyType = "EnumMapTo", + Value = [ + new EnumProperty { Name = "EnumMapToEnum", PropertyType = "TestEnum", Value = new HashSet { "TestValue1" } }, + ], + } + ]; + var hasProperties = new TestHasProperties(props); + + // Act + var mappedProperty = hasProperties.GetMappedProperty("ClassInObject"); + + // Assert + Assert.Equal(TestEnum.TestValue1, mappedProperty.EnumMapToEnum); + } + + private enum TestEnumWithFlags + { + TestValue1 = 1, + TestValue2 = 2, + TestValue3 = 4 + } + + private sealed class EnumWithFlagsMapTo + { + public TestEnumWithFlags EnumWithFlagsMapToEnum { get; set; } = TestEnumWithFlags.TestValue1; + } + + [Fact] + public void GetMappedProperty_EnumWithFlagsProperty_ReturnsMappedProperty() + { + // Arrange + List props = [ + new ClassProperty { + Name = "ClassInObject", + PropertyType = "EnumWithFlagsMapTo", + Value = [ + new EnumProperty { Name = "EnumWithFlagsMapToEnum", PropertyType = "TestEnumWithFlags", Value = new HashSet { "TestValue1", "TestValue2" } }, + ], + } + ]; + var hasProperties = new TestHasProperties(props); + + // Act + var mappedProperty = hasProperties.GetMappedProperty("ClassInObject"); + + // Assert + Assert.Equal(TestEnumWithFlags.TestValue1 | TestEnumWithFlags.TestValue2, mappedProperty.EnumWithFlagsMapToEnum); + } +} diff --git a/src/DotTiled/Properties/ClassProperty.cs b/src/DotTiled/Properties/ClassProperty.cs index 2df32ee..7244aa5 100644 --- a/src/DotTiled/Properties/ClassProperty.cs +++ b/src/DotTiled/Properties/ClassProperty.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; namespace DotTiled; @@ -8,7 +6,7 @@ namespace DotTiled; /// /// Represents a class property. /// -public class ClassProperty : IHasProperties, IProperty> +public class ClassProperty : HasPropertiesBase, IProperty> { /// public required string Name { get; set; } @@ -36,30 +34,5 @@ public class ClassProperty : IHasProperties, IProperty> }; /// - public IList GetProperties() => Value; - - /// - public T GetProperty(string name) where T : IProperty - { - var property = Value.FirstOrDefault(_properties => _properties.Name == name) ?? throw new InvalidOperationException($"Property '{name}' not found."); - if (property is T prop) - { - return prop; - } - - throw new InvalidOperationException($"Property '{name}' is not of type '{typeof(T).Name}'."); - } - - /// - public bool TryGetProperty(string name, [NotNullWhen(true)] out T property) where T : IProperty - { - if (Value.FirstOrDefault(_properties => _properties.Name == name) is T prop) - { - property = prop; - return true; - } - - property = default; - return false; - } + public override IList GetProperties() => Value; } diff --git a/src/DotTiled/Properties/IHasProperties.cs b/src/DotTiled/Properties/IHasProperties.cs index 8ffd9f0..748e2b5 100644 --- a/src/DotTiled/Properties/IHasProperties.cs +++ b/src/DotTiled/Properties/IHasProperties.cs @@ -31,6 +31,8 @@ public interface IHasProperties /// The name of the property to get. /// The property with the specified name. T GetProperty(string name) where T : IProperty; + + T GetMappedProperty(string name) where T : new(); } /// @@ -69,4 +71,61 @@ public abstract class HasPropertiesBase : IHasProperties property = default; return false; } + + public T GetMappedProperty(string name) where T : new() + { + var property = GetProperty(name); + return CreateMappedInstance(property); + } + + private static object CreatedMappedInstance(Type type, ClassProperty classProperty) + { + var instance = Activator.CreateInstance(type); + + foreach (var prop in classProperty.Value) + { + if (type.GetProperty(prop.Name) == null) + throw new KeyNotFoundException($"Property '{prop.Name}' not found in '{type.Name}'."); + + switch (prop) + { + case BoolProperty boolProp: + type.GetProperty(prop.Name)?.SetValue(instance, boolProp.Value); + break; + case ColorProperty colorProp: + type.GetProperty(prop.Name)?.SetValue(instance, colorProp.Value); + break; + case FloatProperty floatProp: + type.GetProperty(prop.Name)?.SetValue(instance, floatProp.Value); + break; + case FileProperty fileProp: + type.GetProperty(prop.Name)?.SetValue(instance, fileProp.Value); + break; + case IntProperty intProp: + type.GetProperty(prop.Name)?.SetValue(instance, intProp.Value); + break; + case ObjectProperty objectProp: + type.GetProperty(prop.Name)?.SetValue(instance, objectProp.Value); + break; + case StringProperty stringProp: + type.GetProperty(prop.Name)?.SetValue(instance, stringProp.Value); + break; + case ClassProperty classProp: + var subClassProp = type.GetProperty(prop.Name); + subClassProp?.SetValue(instance, CreatedMappedInstance(subClassProp.PropertyType, classProp)); + break; + case EnumProperty enumProp: + var enumPropInClass = type.GetProperty(prop.Name); + var enumType = enumPropInClass?.PropertyType; + enumPropInClass?.SetValue(instance, Enum.Parse(enumType!, string.Join(", ", enumProp.Value))); + break; + default: + throw new ArgumentOutOfRangeException($"Unknown property type {prop.GetType().Name}"); + } + } + + return instance; + } + + private static T CreateMappedInstance(ClassProperty classProperty) where T : new() => (T)CreatedMappedInstance(typeof(T), classProperty); }