diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs new file mode 100644 index 00000000..f2ef5db7 --- /dev/null +++ b/ObjectPrinting/ObjectExtensions.cs @@ -0,0 +1,9 @@ +namespace ObjectPrinting; + +public static class ObjectExtensions +{ + public static string PrintToString(this T obj) + { + return ObjectPrinter.For().PrintToString(obj); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392f..e2f22aab 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..9cbccd1a 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,11 +1,30 @@ 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 { public class PrintingConfig { + private readonly HashSet excludedTypes = new(); + private readonly HashSet excludedFields = new(); + private readonly HashSet proccesedObjects = new(); + private readonly Dictionary> alternativeSerializeTypes = new(); + private readonly Dictionary> alternativeSerializeProperties = new(); + private readonly Dictionary alternativeCulture = new(); + private readonly Dictionary lengthOfProperties = new(); + + private readonly Type[] finalTypes = + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid) + }; + public string PrintToString(TOwner obj) { return PrintToString(obj, 0); @@ -14,28 +33,160 @@ public string PrintToString(TOwner obj) private string PrintToString(object obj, int nestingLevel) { //TODO apply configurations + var indentation = new string('\t', nestingLevel + 1); + + var specialSerialization = TryGetSerializationString(obj, nestingLevel); + if (specialSerialization != null) + return specialSerialization; + + proccesedObjects.Add(obj); + var sb = new StringBuilder(); + var type = obj.GetType(); + sb.AppendLine(type.Name); + foreach (var propertyInfo in type.GetProperties()) + { + string property = indentation + propertyInfo.Name + " = "; + if (excludedTypes.Contains(propertyInfo.PropertyType) || excludedFields.Contains(propertyInfo)) + continue; + if (lengthOfProperties.TryGetValue(propertyInfo, out var length)) + { + var substring = propertyInfo.GetValue(obj).ToString().Substring(0, length); + sb.Append(property + substring + Environment.NewLine); + continue; + } + + string serializedObj; + if (alternativeSerializeProperties.TryGetValue(propertyInfo, out var propertyFunc)) + serializedObj = property + propertyFunc(propertyInfo.GetValue(obj)) + Environment.NewLine; + else + serializedObj = property + PrintToString(propertyInfo.GetValue(obj), nestingLevel + 1); + + sb.Append(serializedObj); + } + + return sb.ToString(); + } + + + private string TryGetSerializationString(object? obj, int nestingLevel) + { if (obj == null) return "null" + Environment.NewLine; + if (excludedTypes.Contains(obj.GetType())) + return string.Empty; + if (proccesedObjects.Contains(obj)) + return "Cyclic reference" + Environment.NewLine; + if (alternativeSerializeTypes.TryGetValue(obj.GetType(), out var serializeFunc)) + return serializeFunc(obj) + Environment.NewLine; - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; + if (obj is IFormattable formObj && alternativeCulture.TryGetValue(obj.GetType(), out var newCulture)) + return formObj.ToString("N", newCulture) + Environment.NewLine; if (finalTypes.Contains(obj.GetType())) return obj + Environment.NewLine; + if (obj is IDictionary dictionary) + return PrintDictionary(dictionary, nestingLevel); + if (obj is IEnumerable collection) + return PrintCollection(collection, nestingLevel); + + return null; + } - var identation = new string('\t', nestingLevel + 1); + private string PrintCollection(IEnumerable collection, int nestingLevel) + { var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + var indentation = new string('\t', nestingLevel + 1); + + sb.Append(indentation + collection.GetType().Name + "{" + Environment.NewLine); + int index = 0; + foreach (var item in collection) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + sb.Append(indentation + "\t[" + index + "] = "); + sb.Append(GetStringWithNestingLevel(item, nestingLevel + 2).Trim()); + sb.Append(Environment.NewLine); + index++; } + + sb.Append(indentation + "}"); return sb.ToString(); } + + private string PrintDictionary(IDictionary dictionary, int nestingLevel) + { + var sb = new StringBuilder(); + var indentation = new string('\t', nestingLevel + 1); + sb.Append(indentation + dictionary.GetType().Name + "{" + Environment.NewLine); + foreach (DictionaryEntry element in dictionary) + { + var key = GetStringWithNestingLevel(element.Key, nestingLevel + 1); + var value = GetStringWithNestingLevel(element.Value, nestingLevel + 2).Trim(); + sb.Append(key + " : " + value + Environment.NewLine); + } + + sb.Append(indentation + "}"); + return sb.ToString(); + } + + private string GetStringWithNestingLevel(object obj, int nestingLevel) + { + var indentation = new string('\t', nestingLevel + 1); + if (finalTypes.Contains(obj.GetType())) + return indentation + obj; + var result = PrintToString(obj, nestingLevel); + return Environment.NewLine + indentation + result; + } + + public PrintingConfig Exclude() + { + excludedTypes.Add(typeof(T)); + return this; + } + + public PrintingConfig Exclude(Expression> func) + { + var propertyInfo = GetPropertyInfo(func); + excludedFields.Add(propertyInfo); + return this; + } + + public PropertyPrintingConfig Printing() + { + return new PropertyPrintingConfig(this, null); + } + + public PropertyPrintingConfig Printing( + Expression> func) + { + var propertyInfo = GetPropertyInfo(func); + return new PropertyPrintingConfig(this, propertyInfo); + } + + private PropertyInfo GetPropertyInfo(Expression> propertyExpression) + { + var propertyName = ((MemberExpression) propertyExpression.Body).Member.Name; + var propertyInfo = typeof(TOwner).GetProperty(propertyName); + if (propertyInfo == null) + throw new Exception($"Property {propertyName} could not be found."); + return propertyInfo; + } + + internal void AddSerializedProperty(PropertyInfo propertyInfo, Func serializeFunc) + { + alternativeSerializeProperties[propertyInfo] = o => serializeFunc((T) o); + } + + internal void AddLengthOfProperty(PropertyInfo propertyInfo, int length) + { + lengthOfProperties[propertyInfo] = length; + } + + internal void AddAlternativeCulture(Type type, CultureInfo cultureInfo) + { + alternativeCulture[type] = cultureInfo; + } + + internal void AddSerializedType(Func serializeFunc) + { + alternativeSerializeTypes[typeof(T)] = o => serializeFunc((T) o); + } } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 00000000..9a70fec5 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,41 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace ObjectPrinting; + +public class PropertyPrintingConfig +{ + internal PrintingConfig PrintingConfig { get; } + internal readonly PropertyInfo? PropertyInfo; + + + public PropertyPrintingConfig(PrintingConfig printingConfig, PropertyInfo propertyInfo) + { + PrintingConfig = printingConfig; + PropertyInfo = propertyInfo; + } + + public PrintingConfig Using(Func newSerialize) + { + return PropertyInfo == null ? SetSerializedType(newSerialize) : SetSerializedProp(newSerialize); + } + + public PrintingConfig Using(CultureInfo cultureInfo) + { + PrintingConfig.AddAlternativeCulture(typeof(TPropType), cultureInfo); + return PrintingConfig; + } + + private PrintingConfig SetSerializedProp(Func newSerialize) + { + PrintingConfig.AddSerializedProperty(PropertyInfo!, newSerialize); + return PrintingConfig; + } + + private PrintingConfig SetSerializedType(Func newSerialize) + { + PrintingConfig.AddSerializedType(newSerialize); + return PrintingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs new file mode 100644 index 00000000..1e06b438 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,15 @@ +using System; + + +namespace ObjectPrinting; + +public static class PropertyPrintingConfigExtensions +{ + public static PrintingConfig TrimmedToLength( + this PropertyPrintingConfig propertyConfig, + int maxLen) + { + propertyConfig.PrintingConfig.AddLengthOfProperty(propertyConfig.PropertyInfo, maxLen); + return propertyConfig.PrintingConfig; + } +} \ 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/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs deleted file mode 100644 index f9555955..00000000 --- a/ObjectPrinting/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Tests -{ - public class Person - { - public Guid Id { get; set; } - public string Name { get; set; } - public double Height { get; set; } - public int Age { get; set; } - } -} \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrintingTests.cs b/ObjectPrintingTests/ObjectPrintingTests.cs new file mode 100644 index 00000000..4c514684 --- /dev/null +++ b/ObjectPrintingTests/ObjectPrintingTests.cs @@ -0,0 +1,198 @@ +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; +using ObjectPrinting; +using ObjectPrinting.Tests; + +namespace ObjectPrintingTests; + +public class ObjectPrintingTests +{ + public class ObjectPrinterTests + { + private static readonly Person Person = new() + {Name = "Monkey", SecondName = "D.Luffy", NameOfPet = "Usopp", Height = 168.8, Age = 17}; + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithExcludingType() + { + var printer = ObjectPrinter.For() + .Exclude(); + + var actual = printer.PrintToString(Person); + + actual.Should().NotContain(Person.Name) + .And.NotContain(Person.SecondName) + .And.NotContain(Person.NameOfPet); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithExcludingField() + { + var printer = ObjectPrinter.For() + .Exclude(p => p.SecondName); + + var actual = printer.PrintToString(Person); + + actual.Should().NotContain(Person.SecondName); + } + + + [TestCase("en-US", "168.8")] + [TestCase("ru-RU", "168,8")] + public void ObjectPrinter_ShouldCorrectPrint_WithSelectedCulture(string culture, string expectedValue) + { + var printer = ObjectPrinter.For() + .Printing().Using(new CultureInfo(culture)); + + var actual = printer.PrintToString(Person); + + actual.Should().Contain(expectedValue); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithSpecialSerializeType() + { + var printer = ObjectPrinter.For() + .Printing().Using((x) => $"~~ {x} ~~"); + + var actual = printer.PrintToString(Person); + + actual.Should().Contain("~~ 168,8 ~~"); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithSpecialSerializeField() + { + var printer = ObjectPrinter.For() + .Printing(p => p.Age).Using(x => 22.ToString()); + + var actual = printer.PrintToString(Person); + + actual.Should().Contain("22"); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithTrimmedStringField() + { + var printer = ObjectPrinter.For() + .Printing(p => p.SecondName).TrimmedToLength(1); + + var actual = printer.PrintToString(Person); + + actual.Should().Contain("D").And.NotContain("D.Luffy"); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithExcludingPerson() + { + var printer = ObjectPrinter.For() + .Exclude(); + + var actual = printer.PrintToString(Person); + + actual.Should().BeEmpty(); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithDictionary() + { + var dict = new Dictionary + { + {"Robin", "Archeologist"}, + {"Usopp", "Cannoneer"}, + {"Luffy", "Captain"} + }; + var expected = "\tDictionary`2{" + Environment.NewLine + + "\t\tRobin : Archeologist" + Environment.NewLine + + "\t\tUsopp : Cannoneer" + Environment.NewLine + + "\t\tLuffy : Captain" + Environment.NewLine + + "\t}"; + + var actual = dict.PrintToString(); + + actual.Should().Contain(expected); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithDictionaryContainsPerson() + { + var dict = new Dictionary + { + {"Archeologist", Person}, + }; + var expected = "\tDictionary`2{" + Environment.NewLine + + "\t\tArcheologist : Person" + Environment.NewLine + + "\t\t\tId = 00000000-0000-0000-0000-000000000000" + Environment.NewLine + + "\t\t\tName = Monkey" + Environment.NewLine + + "\t\t\tSecondName = D.Luffy" + Environment.NewLine + + "\t\t\tNameOfPet = Usopp" + Environment.NewLine + + "\t\t\tHeight = 168,8" + Environment.NewLine + + "\t\t\tAge = 17" + Environment.NewLine + + "\t\t\tCountsOfTeamMembers = null" + Environment.NewLine + + "\t\t\tAlliedTeams = null" + Environment.NewLine + + "\t\t\tTeam = null" + Environment.NewLine + + "\t}"; + + var actual = dict.PrintToString(); + + actual.Should().Contain(expected); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithArray() + { + int[] numbers = {1, 2, 3, 4, 5}; + var expected = "\tInt32[]{" + Environment.NewLine + + "\t\t[0] = 1" + Environment.NewLine + + "\t\t[1] = 2" + Environment.NewLine + + "\t\t[2] = 3" + Environment.NewLine + + "\t\t[3] = 4" + Environment.NewLine + + "\t\t[4] = 5" + Environment.NewLine + + "\t}"; + + var actual = numbers.PrintToString(); + + actual.Should().Contain(expected); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithList() + { + List members = new List {"Robin", "Usopp", "Luffy", "Zoro", "Nami"}; + var expected = "\tList`1{" + Environment.NewLine + + "\t\t[0] = Robin" + Environment.NewLine + + "\t\t[1] = Usopp" + Environment.NewLine + + "\t\t[2] = Luffy" + Environment.NewLine + + "\t\t[3] = Zoro" + Environment.NewLine + + "\t\t[4] = Nami" + Environment.NewLine + + "\t}"; + + var actual = members.PrintToString(); + + actual.Should().Contain(expected); + } + + [Test] + public void ObjectPrinter_ShouldCorrectPrint_WithLContainsPerson() + { + List members = new List {Person}; + var expected = "\tList`1{" + Environment.NewLine + + "\t\t[0] = Person" + Environment.NewLine + + "\t\t\tId = 00000000-0000-0000-0000-000000000000" + Environment.NewLine + + "\t\t\tName = Monkey" + Environment.NewLine + + "\t\t\tSecondName = D.Luffy" + Environment.NewLine + + "\t\t\tNameOfPet = Usopp" + Environment.NewLine + + "\t\t\tHeight = 168,8" + Environment.NewLine + + "\t\t\tAge = 17" + Environment.NewLine + + "\t\t\tCountsOfTeamMembers = null" + Environment.NewLine + + "\t\t\tAlliedTeams = null" + Environment.NewLine + + "\t\t\tTeam = null" + Environment.NewLine + + "\t}"; + + var actual = members.PrintToString(); + + actual.Should().Contain(expected); + } + } +} \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrintingTests.csproj b/ObjectPrintingTests/ObjectPrintingTests.csproj new file mode 100644 index 00000000..4ff99ca3 --- /dev/null +++ b/ObjectPrintingTests/ObjectPrintingTests.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/ObjectPrintingTests/Person.cs b/ObjectPrintingTests/Person.cs new file mode 100644 index 00000000..4de57128 --- /dev/null +++ b/ObjectPrintingTests/Person.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting.Tests +{ + public class Person + { + public Guid Id { get; set; } + public string Name { get; set; } + public string SecondName { get; set; } + public string NameOfPet { get; set; } + public double Height { get; set; } + public int Age { get; set; } + + public int[] CountsOfTeamMembers { get; set; } + + public List AlliedTeams { get; set; } + + public Dictionary Team { get; set; } + } +} \ No newline at end of file