diff --git a/ObjectPrinting/Helper.cs b/ObjectPrinting/Helper.cs new file mode 100644 index 00000000..b8eb9964 --- /dev/null +++ b/ObjectPrinting/Helper.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting +{ + public static class Helper + { + public static PropertyInfo GetPropertyInfo( + Expression> propertyLambda) + { + if (propertyLambda.Body is not MemberExpression member) + { + throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property."); + } + + if (member.Member is not PropertyInfo propInfo) + { + throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); + } + + var type = typeof(TSource); + if (propInfo.ReflectedType != null && type != propInfo.ReflectedType && + !type.IsSubclassOf(propInfo.ReflectedType)) + { + throw new ArgumentException( + $"Expression '{propertyLambda}' refers to a property that is not from type {type}."); + } + + return propInfo; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/IBaseConfig.cs b/ObjectPrinting/IBaseConfig.cs new file mode 100644 index 00000000..4e2867a3 --- /dev/null +++ b/ObjectPrinting/IBaseConfig.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq.Expressions; + +namespace ObjectPrinting +{ + public interface IBaseConfig + { + string PrintToString(TOwner obj); + IBaseConfig Exclude(); + IBaseConfig Exclude(Expression> f); + TypeConfig Printing(); + PropertyConfig Printing(Expression> f); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index 3c7867c3..cabb9a60 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -2,7 +2,7 @@ namespace ObjectPrinting { public class ObjectPrinter { - public static PrintingConfig For() + public static IBaseConfig For() { return new PrintingConfig(); } diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index 1c5eaf1c..a80abb23 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -1,12 +1,13 @@  - 8 + latest netcoreapp3.1 false + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..ea4fa696 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,173 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text; namespace ObjectPrinting { - public class PrintingConfig + public class PrintingConfig : + IBaseConfig { + private readonly Type[] finalTypes = new[] + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid) + }; + + private readonly HashSet objects; + + private readonly List excludedTypes; + private readonly List excludedProperties; + + private readonly Dictionary> serializedByType; + private readonly Dictionary> serializedByPropertyInfo; + + public PrintingConfig() + { + objects = new HashSet(); + excludedTypes = new List(); + excludedProperties = new List(); + serializedByType = new Dictionary>(); + serializedByPropertyInfo = new Dictionary>(); + } + public string PrintToString(TOwner obj) { + objects.Clear(); return PrintToString(obj, 0); } private string PrintToString(object obj, int nestingLevel) { - //TODO apply configurations if (obj == null) return "null" + Environment.NewLine; - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; if (finalTypes.Contains(obj.GetType())) return obj + Environment.NewLine; + if (objects.Contains(obj)) + return "cycled" + Environment.NewLine; + objects.Add(obj); + var identation = new string('\t', nestingLevel + 1); var sb = new StringBuilder(); var type = obj.GetType(); sb.AppendLine(type.Name); foreach (var propertyInfo in type.GetProperties()) { + var propType = propertyInfo.PropertyType; + + if (excludedProperties.Contains(propertyInfo) || excludedTypes.Contains(propType)) + continue; + + if (serializedByPropertyInfo.TryGetValue(propertyInfo, out var propertySerialization)) + { + var serializedValue = propertySerialization(propertyInfo.GetValue(obj)); + sb.Append(identation + propertyInfo.Name + " = " + serializedValue); + continue; + } + + if (serializedByType.TryGetValue(propType, out var typeSerialization)) + { + var serializedValue = typeSerialization(propertyInfo.GetValue(obj)); + sb.Append(identation + propertyInfo.Name + " = " + serializedValue); + continue; + } + + if (propType.GetInterfaces().Contains(typeof(IList))) + { + sb.Append(identation + propertyInfo.Name + ": "); + sb.Append(SerializeListElements(propertyInfo.GetValue(obj), nestingLevel + 1)); + continue; + } + + if (propType.GetInterfaces().Contains(typeof(IDictionary))) + { + sb.Append(identation + propertyInfo.Name + ": "); + sb.Append(SerializeDictionaryElements(propertyInfo.GetValue(obj), nestingLevel + 1)); + continue; + } + sb.Append(identation + propertyInfo.Name + " = " + PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1)); } + return sb.ToString(); } + + private string SerializeDictionaryElements(object obj, int nesting) + { + var sb = new StringBuilder(); + var dictionary = obj as IDictionary; + var identation = new string('\t', nesting + 1); + if (dictionary == null || dictionary.Keys.Count == 0) + return "" + Environment.NewLine; + sb.Append(Environment.NewLine); + var index = 0; + foreach (var element in dictionary.Keys) + { + sb.AppendLine(identation + index++ + " element:"); + sb.Append(identation + "\tKey: " + PrintToString(element, nesting + 2)); + sb.Append(identation + "\tValue: " + PrintToString(dictionary[element], nesting + 2)); + } + + return sb.ToString(); + } + + private string SerializeListElements(object obj, int nesting) + { + var sb = new StringBuilder(); + var list = obj as IList; + var identation = new string('\t', nesting + 1); + if (list == null || list.Count == 0) + return "" + Environment.NewLine; + sb.Append(Environment.NewLine); + var index = 0; + foreach (var element in list) + { + sb.Append(identation + index++ + ": " + PrintToString(element, nesting + 1)); + } + + return sb.ToString(); + } + + public IBaseConfig Exclude(Expression> f) + { + var configuratedProperty = Helper.GetPropertyInfo(f); + excludedProperties.Add(configuratedProperty); + return this; + } + + public IBaseConfig Exclude() + { + excludedTypes.Add(typeof(T)); + return this; + } + + public TypeConfig Printing() + { + var configuratedType = typeof(TArg); + return new TypeConfig(this, configuratedType); + } + + public PropertyConfig Printing(Expression> f) + { + var configuratedProperty = Helper.GetPropertyInfo(f); + return new PropertyConfig(this, configuratedProperty); + } + + public void AddPropertySerialization(Func f, PropertyInfo configuratedProperty) + { + serializedByPropertyInfo.Add(configuratedProperty, obj => f((TProperty)obj)); + } + + public void AddTypeSerialization(Func f, Type configuratedType) + { + serializedByType.Add(configuratedType, obj => f((TProperty)obj)); + } } } \ No newline at end of file diff --git a/ObjectPrinting/PrintingExtension.cs b/ObjectPrinting/PrintingExtension.cs new file mode 100644 index 00000000..c1e51982 --- /dev/null +++ b/ObjectPrinting/PrintingExtension.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting +{ + public static class PrintingExtension + { + public static PrintingConfig Truncate( + this PropertyConfig config, + int startPos, + int length + ) + { + return config.SerializeAs(s => s.Substring(startPos, length)); + } + + public static PrintingConfig SetCulture( + this TypeConfig config, + CultureInfo info + ) where TResult : IFormattable + { + return config.SerializeAs(obj => obj.ToString("", info)); + } + + public static PrintingConfig SetCulture( + this PropertyConfig config, + CultureInfo info + ) where TResult : IFormattable + { + return config.SerializeAs(obj => obj.ToString("", info)); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyConfig.cs b/ObjectPrinting/PropertyConfig.cs new file mode 100644 index 00000000..538a91a9 --- /dev/null +++ b/ObjectPrinting/PropertyConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting +{ + public class PropertyConfig + { + private readonly PrintingConfig config; + private readonly PropertyInfo configuratedProperty; + + public PropertyConfig(PrintingConfig config, PropertyInfo configuratedProperty) + { + this.config = config; + this.configuratedProperty = configuratedProperty; + } + + public PrintingConfig SerializeAs(Func f) + { + config.AddPropertySerialization(f, configuratedProperty); + return config; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index 4c8b2445..00000000 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NUnit.Framework; - -namespace ObjectPrinting.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For(); - //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/TypeConfig.cs b/ObjectPrinting/TypeConfig.cs new file mode 100644 index 00000000..973c19a3 --- /dev/null +++ b/ObjectPrinting/TypeConfig.cs @@ -0,0 +1,22 @@ +using System; + +namespace ObjectPrinting +{ + public class TypeConfig + { + private readonly PrintingConfig config; + private readonly Type configuratedType; + + public TypeConfig(PrintingConfig config, Type configuratedType) + { + this.config = config; + this.configuratedType = configuratedType; + } + + public PrintingConfig SerializeAs(Func f) + { + config.AddTypeSerialization(f, configuratedType); + return config; + } + } +} \ No newline at end of file diff --git a/ObjectPrintingTest/GlobalUsings.cs b/ObjectPrintingTest/GlobalUsings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/ObjectPrintingTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/ObjectPrintingTest/ObjectPrinterAcceptanceTests.cs b/ObjectPrintingTest/ObjectPrinterAcceptanceTests.cs new file mode 100644 index 00000000..198db0d1 --- /dev/null +++ b/ObjectPrintingTest/ObjectPrinterAcceptanceTests.cs @@ -0,0 +1,213 @@ +using System.Globalization; +using FluentAssertions; +using ObjectPrinting; +using ObjectPrintingTest.TestTypes; + +namespace ObjectPrintingTest +{ + [TestFixture] + public class ObjectPrinterAcceptanceTests + { + private IBaseConfig sut; + + [SetUp] + public void SetUp() + { + sut = ObjectPrinter.For(); + } + + [Test] + public void ExcludeType() + { + var person = new Person { Name = "Alex", Age = 19, Height = 1.23 }; + sut + .Exclude() + .Exclude() + .PrintToString(person) + .Should() + .NotContainAll("Height", "Age"); + } + + [Test] + public void ExcludeProperty() + { + var person = new Person { Name = "Alex", Age = 19 }; + sut + .Exclude(p => p.Name) + .Exclude(p => p.Age) + .PrintToString(person) + .Should() + .NotContainAll(new[] { "Name", "Age" }); + } + + [Test] + public void SerializeType() + { + var person = new Person { Name = "Alex", Age = 19 }; + sut + .Printing() + .SerializeAs(s => s + s) + .PrintToString(person) + .Should() + .Contain("Name = AlexAlex"); + } + + [Test] + public void SerializeProperty() + { + var person = new Person { Name = "Alex", Age = 19 }; + sut + .Printing(p => p.Age) + .SerializeAs(age => $"{age * 3}") + .PrintToString(person) + .Should() + .Contain("Age = 57"); + } + + [Test] + public void Truncate() + { + var person = new Person { Name = "Alex", Age = 19 }; + sut + .Printing(p => p.Name) + .Truncate(1, 3) + .PrintToString(person) + .Should() + .Contain("Name = lex"); + } + + [Test] + public void CultureInfoForIFormattableType() + { + var person = new Person { Name = "Alex", Age = 19, Height = 1.23 }; + sut + .PrintToString(person) + .Should() + .Contain("Height = 1,23"); + sut + .Printing() + .SetCulture(CultureInfo.InvariantCulture) + .PrintToString(person) + .Should() + .Contain("Height = 1.23"); + } + + [Test] + public void CultureInfoForIFormattableProperty() + { + var person = new Person { Name = "Alex", Age = 19, Height = 1.23 }; + sut + .PrintToString(person) + .Should() + .Contain("Height = 1,23"); + sut + .Printing(p => p.Height) + .SetCulture(CultureInfo.InvariantCulture) + .PrintToString(person) + .Should() + .Contain("Height = 1.23"); + } + + [Test] + public void PrintList() + { + var node = new Node( + "A", + new List + { + new Node("B"), + new Node( + "C", + new List() + { + new Node("D"), + new Node("E") + } + ) + }); + var result = ObjectPrinter.For().PrintToString(node); + Console.WriteLine(result); + result + .Should() + .Be( + """ + Node + Name = A + Nodes: + 0: Node + Name = B + Nodes: + 1: Node + Name = C + Nodes: + 0: Node + Name = D + Nodes: + 1: Node + Name = E + Nodes: + + """); + } + + [Test] + public void PrintDictionaries() + { + var phone = new PhoneBook( + "New York", + new Dictionary() + { + { "+1 (646) 555-3456", new Person() { Name = "John Doe", Age = 30 } }, + { "+1 (646) 555-4567", new Person() { Name = "Jane Doe", Age = 30 } } + } + ); + var result = ObjectPrinter.For().PrintToString(phone); + Console.WriteLine(result); + result + .Should() + .Be( + """ + PhoneBook + Town = New York + NumberToPerson: + 0 element: + Key: +1 (646) 555-3456 + Value: Person + Id = 00000000-0000-0000-0000-000000000000 + Name = John Doe + Height = 0 + Age = 30 + 1 element: + Key: +1 (646) 555-4567 + Value: Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Jane Doe + Height = 0 + Age = 30 + + """); + } + + [Test] + public void Cycled() + { + var node = new Node("A").Add(new Node("B")); + node.Add(node); + var result = ObjectPrinter.For().PrintToString(node); + Console.WriteLine(result); + result + .Should() + .Be( + """ + Node + Name = A + Nodes: + 0: Node + Name = B + Nodes: + 1: cycled + + """); + } + } +} \ No newline at end of file diff --git a/ObjectPrintingTest/ObjectPrintingTest.csproj b/ObjectPrintingTest/ObjectPrintingTest.csproj new file mode 100644 index 00000000..085ca480 --- /dev/null +++ b/ObjectPrintingTest/ObjectPrintingTest.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + enable + + false + true + latest + + + + + + + + + + + + + + + diff --git a/ObjectPrintingTest/TestTypes/Node.cs b/ObjectPrintingTest/TestTypes/Node.cs new file mode 100644 index 00000000..0b2cb4de --- /dev/null +++ b/ObjectPrintingTest/TestTypes/Node.cs @@ -0,0 +1,25 @@ +namespace ObjectPrintingTest.TestTypes; + +public class Node +{ + public string Name { get; set; } + public List Nodes { get; private set; } + + public Node(string name) + { + Name = name; + Nodes = new List(); + } + + public Node(string name, List nodes) + { + Name = name; + Nodes = nodes; + } + + public Node Add(Node node) + { + Nodes.Add(node); + return this; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrintingTest/TestTypes/Person.cs similarity index 80% rename from ObjectPrinting/Tests/Person.cs rename to ObjectPrintingTest/TestTypes/Person.cs index f9555955..9e344601 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrintingTest/TestTypes/Person.cs @@ -1,6 +1,4 @@ -using System; - -namespace ObjectPrinting.Tests +namespace ObjectPrintingTest.TestTypes { public class Person { diff --git a/ObjectPrintingTest/TestTypes/PhoneBook.cs b/ObjectPrintingTest/TestTypes/PhoneBook.cs new file mode 100644 index 00000000..f9cc86ef --- /dev/null +++ b/ObjectPrintingTest/TestTypes/PhoneBook.cs @@ -0,0 +1,13 @@ +namespace ObjectPrintingTest.TestTypes; + +public class PhoneBook +{ + public PhoneBook(string town, Dictionary numberToPerson) + { + Town = town; + NumberToPerson = numberToPerson; + } + + public string Town { get; set; } + public Dictionary NumberToPerson { get; set; } +} \ No newline at end of file diff --git a/Samples/FluentMapper.Tests/FluentMapping.Tests.csproj b/Samples/FluentMapper.Tests/FluentMapping.Tests.csproj index 0d5262d4..08ab415a 100644 --- a/Samples/FluentMapper.Tests/FluentMapping.Tests.csproj +++ b/Samples/FluentMapper.Tests/FluentMapping.Tests.csproj @@ -1,7 +1,7 @@  - 8 + latest netcoreapp3.1 false diff --git a/Samples/FluentMapper/FluentMapping.csproj b/Samples/FluentMapper/FluentMapping.csproj index d7f3a084..b871c4c9 100644 --- a/Samples/FluentMapper/FluentMapping.csproj +++ b/Samples/FluentMapper/FluentMapping.csproj @@ -1,7 +1,7 @@  - 8 + latest netcoreapp3.1 false diff --git a/Samples/Spectacle/Spectacle.csproj b/Samples/Spectacle/Spectacle.csproj index 6fcb3e8a..dc02c463 100644 --- a/Samples/Spectacle/Spectacle.csproj +++ b/Samples/Spectacle/Spectacle.csproj @@ -1,7 +1,7 @@  - 8 + latest netcoreapp3.1 Exe SpectacleSample diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9e..c4ebcd5a 100644 --- a/fluent-api.sln +++ b/fluent-api.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMapping.Tests", "Samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectacle", "Samples\Spectacle\Spectacle.csproj", "{EFA9335C-411B-4597-B0B6-5438D1AE04C3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectPrintingTest", "ObjectPrintingTest\ObjectPrintingTest.csproj", "{BAF4655C-D2BF-410C-90E3-02E09668406C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +37,10 @@ Global {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {EFA9335C-411B-4597-B0B6-5438D1AE04C3}.Release|Any CPU.Build.0 = Release|Any CPU + {BAF4655C-D2BF-410C-90E3-02E09668406C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAF4655C-D2BF-410C-90E3-02E09668406C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAF4655C-D2BF-410C-90E3-02E09668406C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAF4655C-D2BF-410C-90E3-02E09668406C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE