diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392ff..ea98111e3 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs index 0ec5aeb2b..445f64612 100644 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ b/ObjectPrinting/Solved/PrintingConfig.cs @@ -1,62 +1,240 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public class PrintingConfig { - public class PrintingConfig + private readonly HashSet excludedTypes = []; + private readonly HashSet excludedMembers = []; + private readonly Dictionary> typeSerializers = new(); + private readonly Dictionary> memberSerializers = new(); + private readonly Dictionary cultures = new(); + private readonly Dictionary stringTrimLengths = new(); + private readonly HashSet visitedObjects = []; + + public PropertyPrintingConfig Printing() + { + return new PropertyPrintingConfig(this); + } + + public PropertyPrintingConfig Printing( + Expression> memberSelector) + { + var member = GetMemberInfo(memberSelector); + return new PropertyPrintingConfig(this, member.Name); + } + + public PrintingConfig Excluding() + { + excludedTypes.Add(typeof(TPropType)); + return this; + } + + public PrintingConfig Excluding(Expression> memberSelector) + { + var member = GetMemberInfo(memberSelector); + excludedMembers.Add(member.Name); + return this; + } + + internal void AddTypeSerializer(Func serializer) + { + typeSerializers[typeof(TPropType)] = obj => serializer((TPropType)obj); + } + + internal void AddMemberSerializer(string memberName, Func serializer) + { + memberSerializers[memberName] = obj => serializer((TPropType)obj); + } + + internal void AddCulture(CultureInfo culture) + { + cultures[typeof(TPropType)] = culture; + } + + internal void SetStringTrimLength(string memberName, int maxLength) + { + stringTrimLengths[memberName] = maxLength; + } + + + public string PrintToString(TOwner obj) { - public PropertyPrintingConfig Printing() + visitedObjects.Clear(); + return PrintToString(obj, 0); + } + + private string PrintToString(object? obj, int nestingLevel) + { + if (obj == null) return "null" + Environment.NewLine; + + if (!visitedObjects.Add(obj)) + return $"Cyclic reference detected ({obj.GetType().Name})" + Environment.NewLine; + + try { - return new PropertyPrintingConfig(this); - } + var type = obj.GetType(); + + if (type == typeof(string)) + { + var str = (string)obj; + return "\"" + str + "\"" + Environment.NewLine; + } + + if (obj is IEnumerable enumerable && !IsFinalType(type)) + { + return PrintCollection(enumerable, nestingLevel); + } - public PropertyPrintingConfig Printing(Expression> memberSelector) + if (IsFinalType(type) || typeSerializers.ContainsKey(type)) + { + return SerializeValue(obj, type) + Environment.NewLine; + } + + return PrintObject(obj, nestingLevel, type); + } + finally { - return new PropertyPrintingConfig(this); + visitedObjects.Remove(obj); } + } + + private static bool IsFinalType(Type type) + { + var finalTypes = new[] + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(bool), + typeof(char), typeof(byte), typeof(short), typeof(long), + typeof(decimal) + }; + return finalTypes.Contains(type) || type.IsEnum || type.IsPrimitive; + } - public PrintingConfig Excluding(Expression> memberSelector) + private string? SerializeValue(object value, Type valueType) + { + try { - return this; + if (typeSerializers.TryGetValue(valueType, out var typeSerializer)) + { + return typeSerializer(value); + } + + if (cultures.TryGetValue(valueType, out var culture) + && value is IFormattable formattable) + return formattable.ToString(null, culture); + return value.ToString(); + } + catch (Exception ex) when (IsSerializationException(ex)) + { + return $"[Serialization error: {ex.Message}]"; } + } + + private static bool IsSerializationException(Exception ex) + { + return ex is FormatException or + InvalidCastException or + NullReferenceException or + ArgumentOutOfRangeException or + DivideByZeroException or + OverflowException or + NotSupportedException or + InvalidOperationException; + } - internal PrintingConfig Excluding() + private string PrintObject(object obj, int nestingLevel, Type type) + { + var indentationLine = new string('\t', nestingLevel + 1); + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(type.Name); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) { - return this; + if (excludedTypes.Contains(field.FieldType) || excludedMembers.Contains(field.Name)) + continue; + stringBuilder.Append(indentationLine + field.Name + " = " + + SerializeMember(field.Name, field.GetValue(obj), nestingLevel)); } - public string PrintToString(TOwner obj) + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (excludedTypes.Contains(property.PropertyType) || excludedMembers.Contains(property.Name)) + continue; + + if (property.GetIndexParameters().Length > 0) + continue; + + stringBuilder.Append(indentationLine + property.Name + " = " + + SerializeMember(property.Name, property.GetValue(obj), nestingLevel)); + } + return stringBuilder.ToString(); + } + private string SerializeMember(string memberName, object? value, int nestingLevel) + { + if (value == null) + return "null" + Environment.NewLine; + + if (memberSerializers.TryGetValue(memberName, out var memberSerializer)) + { + return memberSerializer(value) + Environment.NewLine; + } + + if (value is string strValue && stringTrimLengths.TryGetValue(memberName, out var maxLength)) { - return PrintToString(obj, 0); + var trimmed = strValue.Length <= maxLength ? strValue : strValue[..maxLength]; + return "\"" + trimmed + "\"" + Environment.NewLine; } - private string PrintToString(object obj, int nestingLevel) + var valueType = value.GetType(); + if (typeSerializers.TryGetValue(valueType, out var typeSerializer)) { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + return typeSerializer(value) + Environment.NewLine; + } + + return PrintToString(value, nestingLevel + 1); + } + + private string PrintCollection(IEnumerable collection, int nestingLevel) + { + var stringBuilder = new StringBuilder(); + var indentationLine = new string('\t', nestingLevel + 1); + stringBuilder.AppendLine(collection.GetType().Name); - var finalTypes = new[] + if (collection is IDictionary dictionary) + { + foreach (var key in dictionary.Keys) { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - 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()) + stringBuilder.Append(indentationLine + + $"[{PrintToString(key, nestingLevel + 1).Trim()}] = " + + PrintToString(dictionary[key], nestingLevel + 1)); + } + } + else + { + var index = 0; + foreach (var item in collection) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + stringBuilder.Append(indentationLine + $"[{index}] = " + PrintToString(item, nestingLevel + 1)); + index++; } - return sb.ToString(); } + return stringBuilder.ToString(); + } + + private static MemberInfo GetMemberInfo(Expression> memberSelector) + { + if (memberSelector.Body is MemberExpression memberExpression) + { + return memberExpression.Member; + } + throw new ArgumentException("Expression is not a member access", nameof(memberSelector)); } -} \ No newline at end of file +} diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs index a509697d1..8eb951b75 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfig.cs @@ -1,32 +1,39 @@ using System; using System.Globalization; -namespace ObjectPrinting.Solved -{ - public class PropertyPrintingConfig : IPropertyPrintingConfig - { - private readonly PrintingConfig printingConfig; +namespace ObjectPrinting.Solved; - public PropertyPrintingConfig(PrintingConfig printingConfig) - { - this.printingConfig = printingConfig; - } +public class PropertyPrintingConfig( + PrintingConfig printingConfig, + string? memberName = null) + : IPropertyPrintingConfig +{ + public string? MemberName { get; } = memberName; - public PrintingConfig Using(Func print) + public PrintingConfig Using(Func print) + { + if (MemberName != null) { - return printingConfig; + printingConfig.AddMemberSerializer(MemberName, print); } - - public PrintingConfig Using(CultureInfo culture) + else { - return printingConfig; + printingConfig.AddTypeSerializer(print); } - PrintingConfig IPropertyPrintingConfig.ParentConfig => printingConfig; + return printingConfig; } - public interface IPropertyPrintingConfig + public PrintingConfig Using(CultureInfo culture) { - PrintingConfig ParentConfig { get; } + printingConfig.AddCulture(culture); + return printingConfig; } + + public PrintingConfig ParentConfig => printingConfig; +} + +public interface IPropertyPrintingConfig +{ + PrintingConfig ParentConfig { get; } } \ No newline at end of file diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs index dd3922394..23b8ba32b 100644 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs @@ -1,18 +1,23 @@ using System; -namespace ObjectPrinting.Solved +namespace ObjectPrinting.Solved; + +public static class PropertyPrintingConfigExtensions { - public static class PropertyPrintingConfigExtensions + public static string PrintToString(this T obj, Func, PrintingConfig> config) { - public static string PrintToString(this T obj, Func, PrintingConfig> config) - { - return config(ObjectPrinter.For()).PrintToString(obj); - } + return config(ObjectPrinter.For()).PrintToString(obj); + } - public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) + public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, + int maxLen) + { + var memberName = propConfig.MemberName; + if (memberName != null) { - return ((IPropertyPrintingConfig)propConfig).ParentConfig; + propConfig.ParentConfig.SetStringTrimLength(memberName, maxLen); } + return propConfig.ParentConfig; } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs new file mode 100644 index 000000000..def6e9f1f --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; +using ObjectPrinting.Solved; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterTests +{ + private Person? testPerson; + + [SetUp] + public void SetUp() + { + testPerson = new Person + { + Id = Guid.NewGuid(), + Name = "Alexandra", + Height = 152, + Age = 52 + }; + } + + [Test] + public void PrintToString_SimpleObject_ReturnsAllProperties() + { + var result = testPerson.PrintToString(); + + result.Should().Contain("Person") + .And.Contain("Name = \"Alexandra\"") + .And.Contain("Age = 52") + .And.Contain("Height = 152") + .And.Contain("Id = "); + } + + [Test] + public void PrintToString_ExcludingGuid_ShouldExcludeIdProperty() + { + var result = testPerson.PrintToString(config => config.Excluding()); + + result.Should().NotContain("Id"); + } + + [Test] + public void PrintToString_ExcludingDouble_ShouldExcludeHeightProperty() + { + var result = testPerson.PrintToString(config => config.Excluding()); + + result.Should().NotContain("Height"); + } + + [Test] + public void PrintToString_ExcludingInt_ShouldExcludeAgeProperty() + { + var result = testPerson.PrintToString(config => config.Excluding()); + + result.Should().NotContain("Age"); + } + + [Test] + public void PrintToString_ExcludingString_ShouldExcludeNameProperty() + { + var result = testPerson.PrintToString(config => config.Excluding()); + + result.Should().NotContain("Name"); + } + + [Test] + public void PrintToString_ExcludingSpecificMember_ShouldExcludeOnlyThatProperty() + { + var result = testPerson.PrintToString(config => + config.Excluding(p => p.Height)); + + result.Should().NotContain("Height") + .And.Contain("Name = \"Alexandra\"") + .And.Contain("Age = 52") + .And.Contain("Id = "); + } + + [Test] + public void PrintToString_ExcludingMultipleMembers_ShouldExcludeAllSpecified() + { + var result = testPerson.PrintToString(config => config + .Excluding(p => p.Height) + .Excluding(p => p.Name)); + + result.Should().NotContain("Height").And.NotContain("Name") + .And.Contain("Age = 52") + .And.Contain("Id = "); + } + + [Test] + public void PrintToString_ExcludingMultipleTypesAndMembers_ShouldExcludeAllSpecified() + { + var result = testPerson.PrintToString(config => config + .Excluding() + .Excluding() + .Excluding(p => p.Name)); + + result.Should() + .NotContain("Id").And + .NotContain("Height").And + .NotContain("Name").And + .Contain("Age = 52"); + } + + [Test] + public void PrintToString_PrintingIntWithCustomSerializer_ShouldUseCustomFormat() + { + var result = testPerson.PrintToString(config => + config.Printing().Using(i => $"{i} years")); + + result.Should().Contain("Age = 52 years"); + } + + [Test] + public void PrintToString_PrintingDoubleWithCustomSerializer_ShouldUseCustomFormat() + { + var result = testPerson.PrintToString(config => + config.Printing().Using(d => $"{d:F1} cm")); + + result.Should().Contain("Height = 152,0 cm"); + } + + [Test] + public void PrintToString_PrintingStringWithCustomSerializer_ShouldUseCustomFormat() + { + var result = testPerson.PrintToString(config => + config.Printing().Using(s => s.ToUpper())); + + result.Should().Contain("Name = ALEXANDRA"); + } + + [Test] + public void PrintToString_TrimmedToSmallLength_ShouldTrimNameProperty() + { + var result = testPerson.PrintToString(config => + config.Printing(p => p.Name).TrimmedToLength(3)); + + result.Should().Contain("Name = \"Ale\"") + .And.NotContain("Alexandra"); + } + + [Test] + public void PrintToString_TrimmedToLengthLongLength_ShouldNotChangeName() + { + var result = testPerson.PrintToString(config => + config.Printing(p => p.Name).TrimmedToLength(20)); + + result.Should().Contain("Name = \"Alexandra\""); + } + + [Test] + public void PrintToString_PrintingWithRussianCulture_ShouldFormatDoubleWithComma() + { + var result = testPerson.PrintToString(config => + config.Printing().Using(CultureInfo.GetCultureInfo("ru-RU"))); + + result.Should().ContainAny("Height = 180,5", "Height = 152"); + } + + [Test] + public void PrintToString_PrintingWithUsCulture_ShouldFormatDoubleWithDot() + { + var result = testPerson.PrintToString(config => + config.Printing().Using(CultureInfo.GetCultureInfo("en-US"))); + + result.Should().ContainAny("Height = 180.5", "Height = 152"); + } + + [Test] + public void PrintToString_WithIntArray_ShouldPrintArray() + { + int[] numbers = { 1, 2, 3 }; + + var result = numbers.PrintToString(); + + result.Should().Contain("Int32[]") + .And.Contain("[0] = 1") + .And.Contain("[1] = 2") + .And.Contain("[2] = 3"); + } + + [Test] + public void PrintToString_WithStringArray_ShouldPrintArray() + { + string[] names = { "Sasha", "Misha", "Grisha" }; + + var result = names.PrintToString(); + + result.Should().Contain("String[]") + .And.Contain("[0] = \"Sasha\"") + .And.Contain("[1] = \"Misha\"") + .And.Contain("[2] = \"Grisha\""); + } + + [Test] + public void PrintToString_WithPersonList_ShouldPrintList() + { + var people = new List + { + new Person { Name = "Sasha", Age = 52 }, + new Person { Name = "Misha", Age = 30 } + }; + + var result = people.PrintToString(); + + result.Should().Contain("List`1") + .And.Contain("[0] =") + .And.Contain("[1] =") + .And.Contain("Name = \"Sasha\"") + .And.Contain("Name = \"Misha\"") + .And.Contain("Age = 52") + .And.Contain("Age = 30"); + } + + [Test] + public void PrintToString_WithDictionary_ShouldPrintDictionary() + { + var dict = new Dictionary + { + ["Sasha"] = 52, + ["Masha"] = 30 + }; + + var result = dict.PrintToString(); + + result.Should().Contain("Dictionary`2") + .And.Contain("[\"Sasha\"] = 52") + .And.Contain("[\"Masha\"] = 30"); + } + + [Test] + public void PrintToString_WithCycleReferenceInList_ShouldDetect() + { + var list = new List(); + list.Add(list); + + var result = list.PrintToString(); + + result.Should().Contain("Cyclic reference detected"); + Assert.DoesNotThrow(() => list.PrintToString()); + } + + [Test] + public void PrintToString_WithNullObject_ReturnsNullString() + { + Person nullPerson = null; + var result = nullPerson.PrintToString().Trim(); + + result.Should().Be("null"); + } + + [Test] + public void PrintToString_WithNullProperty_HandlesNull() + { + var personWithNull = new Person { Name = null, Age = 52 }; + var result = personWithNull.PrintToString(); + + result.Should().Contain("Name = null") + .And.Contain("Age = 52"); + } + + [Test] + public void PrintToString_WithNestedObjectsInList_ShouldSerializeRecursively() + { + var people = new List + { + new() { Name = "Sasha", Age = 45 }, + new() { Name = "Masha", Age = 30 } + }; + + var result = people.PrintToString(); + + result.Should().Contain("List`1") + .And .Contain("[0] =") + .And .Contain("[1] =") + .And .Contain("Person") + .And .Contain("Name = \"Sasha\"") + .And .Contain("Age = 45") + .And .Contain("Name = \"Masha\"") + .And .Contain("Age = 30"); + } + + [Test] + public void PrintToString_WithNestedObject_ShouldContainNestedProperties() + { + var office = new Office + { + Address = "Moskovskaya", + Manager = new Person { Name = "Sasha", Age = 44 } + }; + + var company = new Company + { + Name = "Kontur", + Office = office + }; + + var result = company.PrintToString(); + + result.Should().Contain("Name = \"Kontur\"") + .And.Contain("Office") + .And.Contain("Office =") + .And.Contain("Office") + .And.Contain("Address = \"Moskovskaya\"") + .And.Contain("Manager =") + .And.Contain("Person") + .And.Contain("Name = \"Sasha\"") + .And .Contain("Age = 44"); + } + + [Test] + public void PrintToString_WithNestedObject_ShouldIncludeAllLevels() + { + var root = new TreeNode + { + Value = "Root", + Child = new TreeNode + { + Value = "ChildOfRoot", + Child = new TreeNode + { + Value = "Leaf" + } + } + }; + + var result = root.PrintToString(); + + result.Should() .Contain("TreeNode") + .And.Contain("Value = \"Root\"") + .And.Contain("Value = \"ChildOfRoot\"") + .And.Contain("Value = \"Leaf\""); + } + + private class Company + { + public string Name { get; set; } + public Office Office { get; set; } + } + + private class Office + { + public string Address { get; set; } + public Person Manager { get; set; } + } + + private class TreeNode + { + public string Value { get; set; } + public TreeNode Child { get; set; } + } +} diff --git a/fluent-api.sln.DotSettings b/fluent-api.sln.DotSettings index 135b83ecb..53fe49b2f 100644 --- a/fluent-api.sln.DotSettings +++ b/fluent-api.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016