diff --git a/ObjectPrinting/Configs/PrintingConfig.cs b/ObjectPrinting/Configs/PrintingConfig.cs new file mode 100644 index 000000000..e8b8b185f --- /dev/null +++ b/ObjectPrinting/Configs/PrintingConfig.cs @@ -0,0 +1,315 @@ +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 Dictionary typeSerializers; + private readonly Dictionary typeCultures; + private readonly Dictionary propertySerializers; + private readonly HashSet excludedProperties; + private readonly HashSet excludedTypes; + private readonly HashSet visitedObjects = []; + + private readonly Type[] finalTypes = + [ + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(Guid), typeof(decimal), + typeof(long), typeof(short), typeof(byte), typeof(bool), + typeof(char), typeof(sbyte), typeof(ushort), typeof(uint), + typeof(ulong) + ]; + + private int nestingLevel; + + public PrintingConfig() + { + typeSerializers = new Dictionary(); + typeCultures = new Dictionary(); + propertySerializers = new Dictionary(); + excludedProperties = []; + excludedTypes = []; + nestingLevel = 50; + } + + private PrintingConfig( + Dictionary typeSerializers, + Dictionary typeCultures, + Dictionary propertySerializers, + HashSet excludedProperties, + HashSet excludedTypes, + int maxNestingLevel) + { + this.typeSerializers = new Dictionary(typeSerializers); + this.typeCultures = new Dictionary(typeCultures); + this.propertySerializers = new Dictionary(propertySerializers); + this.excludedProperties = new HashSet(excludedProperties); + this.excludedTypes = new HashSet(excludedTypes); + this.nestingLevel = maxNestingLevel; + } + + private PrintingConfig PrintingConfigWith( + Dictionary typeSerializers = null, + Dictionary typeCultures = null, + Dictionary propertySerializers = null, + HashSet excludedProperties = null, + HashSet excludedTypes = null, + int? maxNestingLevel = null) + { + return new PrintingConfig( + typeSerializers ?? this.typeSerializers, + typeCultures ?? this.typeCultures, + propertySerializers ?? this.propertySerializers, + excludedProperties ?? this.excludedProperties, + excludedTypes ?? this.excludedTypes, + maxNestingLevel ?? this.nestingLevel); + } + + public PrintingConfig ExcludeType() + { + var newExcludedTypes = new HashSet(excludedTypes) { typeof(T) }; + return PrintingConfigWith(excludedTypes: newExcludedTypes); + } + + public TypeSerializingConfig SerializeType() + { + return new TypeSerializingConfig(this); + } + + public PropertySerializingConfig SerializeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + return new PropertySerializingConfig(this, propertyName); + } + + public StringPropertySerializingConfig SerializeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + return new StringPropertySerializingConfig(this, propertyName); + } + + public PrintingConfig ExcludeProperty( + Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + var newExcludedProperties = new HashSet(excludedProperties) { propertyName }; + return PrintingConfigWith(excludedProperties: newExcludedProperties); + } + + public string PrintToString(TOwner obj, int? nestingLevel = null) + { + if (nestingLevel < 0) + throw new ArgumentException("Nesting level cannot be negative"); + this.nestingLevel = nestingLevel ?? this.nestingLevel; + + visitedObjects.Clear(); + return PrintToStringInternal(obj, 0); + } + + private string PrintToStringInternal(object obj, int currentnestingLevel) + { + if (obj == null) + return "null" + Environment.NewLine; + + if (currentnestingLevel >= this.nestingLevel) + return $"Max nesting level ({this.nestingLevel}) reached" + Environment.NewLine; + + var type = obj.GetType(); + + if (visitedObjects.Contains(obj)) + return $"Cyclic reference detected ({type.Name})" + Environment.NewLine; + + visitedObjects.Add(obj); + + try + { + if (IsFinalType(type)) + return SerializeValue(obj, type, null) + Environment.NewLine; + + if (obj is IEnumerable enumerable && !(obj is string)) + return SerializeCollection(enumerable, currentnestingLevel); + + return SerializeObject(obj, currentnestingLevel, type); + } + finally + { + visitedObjects.Remove(obj); + } + } + + private string SerializeCollection(IEnumerable collection, int nestingLevel) + { + var indentation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + var type = collection.GetType(); + + var collectionTypeName = type.IsGenericType ? GetGenericTypeName(type) : type.Name; + sb.AppendLine(collectionTypeName); + + var index = 0; + foreach (var item in collection) + { + var serializedItem = SerializeValue(item, + item?.GetType() ?? typeof(object), null, nestingLevel + 1); + + sb.Append($"{indentation}[{index}] = {serializedItem}"); + index++; + } + + if (collection is Array || collection is not ICollection coll) + return sb.ToString(); + + sb.Append($"{indentation}Count = {coll.Count}" + Environment.NewLine); + + return sb.ToString(); + } + + private string GetGenericTypeName(Type type) + { + var genericArgs = type.GetGenericArguments(); + var genericTypeName = type.GetGenericTypeDefinition().Name; + var cleanName = genericTypeName[..genericTypeName.IndexOf('`')]; + var args = string.Join(", ", genericArgs.Select(t => t.Name)); + return $"{cleanName}`{genericArgs.Length}<{args}>"; + } + + + private string SerializeObject(object obj, int nestingLevel, Type type) + { + var indentation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + + foreach (var fieldInfo in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (ShouldExcludeMember(fieldInfo.FieldType, fieldInfo.Name)) + continue; + + var value = fieldInfo.GetValue(obj); + var serializedValue = SerializeValue(value, fieldInfo.FieldType, + fieldInfo.Name, nestingLevel); + + sb.Append(indentation + fieldInfo.Name + " = " + serializedValue); + } + + foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (propertyInfo.GetIndexParameters().Length > 0) + continue; + + if (ShouldExcludeMember(propertyInfo.PropertyType, propertyInfo.Name)) + continue; + + var value = propertyInfo.GetValue(obj); + var serializedValue = SerializeValue(value, propertyInfo.PropertyType, + propertyInfo.Name, nestingLevel); + sb.Append(indentation + propertyInfo.Name + " = " + serializedValue); + } + + return sb.ToString(); + } + + private string SerializeValue(object value, Type valueType, + string memberName, int nestingLevel = 0) + { + if (value == null) + return "null" + Environment.NewLine; + + if (memberName != null && propertySerializers.TryGetValue(memberName, out var propertySerializer)) + { + try + { + var result = propertySerializer.DynamicInvoke(value); + return result?.ToString() + Environment.NewLine; + } + catch + { + // Fall back to default serialization + } + } + + if (typeSerializers.TryGetValue(valueType, out var typeSerializer)) + { + try + { + var result = typeSerializer.DynamicInvoke(value); + return result?.ToString() + Environment.NewLine; + } + catch + { + // Fall back to default serialization + } + } + + if (typeCultures.TryGetValue(valueType, out var culture) && value is IFormattable formattable) + { + return formattable.ToString(null, culture) + Environment.NewLine; + } + + if (!IsFinalType(valueType)) + return PrintToStringInternal(value, nestingLevel + 1); + + if (value is IFormattable formattableDefault) + return formattableDefault.ToString(null, CultureInfo.InvariantCulture) + Environment.NewLine; + + return value.ToString() + Environment.NewLine; + } + + private bool ShouldExcludeMember(Type memberType, string memberName) + { + return excludedTypes.Contains(memberType) || excludedProperties.Contains(memberName); + } + + private bool IsFinalType(Type type) => finalTypes.Contains(type) || type.IsEnum; + + private string GetPropertyName( + Expression> propertySelector) + { + switch (propertySelector.Body) + { + case MemberExpression memberExpression: + return memberExpression.Member.Name; + case UnaryExpression { Operand: MemberExpression unaryMemberExpression }: + return unaryMemberExpression.Member.Name; + default: + throw new ArgumentException("Expression must be a property or field selector"); + } + } + + internal PrintingConfig AddTypeSerializer(Func serializeFunc) + { + var newTypeSerializers = new Dictionary(typeSerializers) + { + [typeof(TType)] = serializeFunc + }; + return PrintingConfigWith(typeSerializers: newTypeSerializers); + } + + internal PrintingConfig AddTypeCulture(CultureInfo cultureInfo) + { + var newTypeCultures = new Dictionary(typeCultures) + { + [typeof(TType)] = cultureInfo + }; + return PrintingConfigWith(typeCultures: newTypeCultures); + } + + internal PrintingConfig AddPropertySerializer(string propertyName, + Func serializer) + { + var newPropertySerializers = new Dictionary(propertySerializers) + { + [propertyName] = serializer + }; + return PrintingConfigWith(propertySerializers: newPropertySerializers); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/PropertySerializingConfig.cs b/ObjectPrinting/Configs/PropertySerializingConfig.cs new file mode 100644 index 000000000..7568719ba --- /dev/null +++ b/ObjectPrinting/Configs/PropertySerializingConfig.cs @@ -0,0 +1,20 @@ +using System; + +namespace ObjectPrinting; + +public class PropertySerializingConfig +{ + protected readonly PrintingConfig printingConfig; + protected readonly string propertyName; + + public PropertySerializingConfig(PrintingConfig printingConfig, string propertyName) + { + this.printingConfig = printingConfig; + this.propertyName = propertyName; + } + + public PrintingConfig Use(Func serializer) + { + return printingConfig.AddPropertySerializer(propertyName, serializer); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/StringPropertySerializingConfig.cs b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs new file mode 100644 index 000000000..88b524360 --- /dev/null +++ b/ObjectPrinting/Configs/StringPropertySerializingConfig.cs @@ -0,0 +1,12 @@ +namespace ObjectPrinting; + +public class StringPropertySerializingConfig : PropertySerializingConfig +{ + public StringPropertySerializingConfig(PrintingConfig printingConfig, string propertyName) + : base(printingConfig, propertyName) { } + + public PrintingConfig TrimTo(int maxLength) + { + return Use(s => s?.Length <= maxLength ? s : s.Substring(0, maxLength)); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/TypeSerializingConfig.cs b/ObjectPrinting/Configs/TypeSerializingConfig.cs new file mode 100644 index 000000000..deebd5d74 --- /dev/null +++ b/ObjectPrinting/Configs/TypeSerializingConfig.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting; + +public class TypeSerializingConfig +{ + private readonly PrintingConfig printingConfig; + + public TypeSerializingConfig(PrintingConfig printingConfig) + { + this.printingConfig = printingConfig; + } + + public PrintingConfig Use(Func serializeFunc) + { + return printingConfig.AddTypeSerializer(serializeFunc); + } + + public PrintingConfig Use(CultureInfo cultureInfo) + { + return printingConfig.AddTypeCulture(cultureInfo); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectExtensions.cs b/ObjectPrinting/ObjectExtensions.cs new file mode 100644 index 000000000..627048339 --- /dev/null +++ b/ObjectPrinting/ObjectExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace ObjectPrinting; + +public static class ObjectPrinterExtensions +{ + public static string PrintToString(this T obj, int? nestingLevel = null) + { + return ObjectPrinter.For().PrintToString(obj, nestingLevel); + } + + public static string PrintToString(this T obj, Func, PrintingConfig> configurator, int? nestingLevel = null) + { + return configurator(ObjectPrinter.For()).PrintToString(obj, nestingLevel); + } +} \ No newline at end of file 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/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs deleted file mode 100644 index a9e082117..000000000 --- a/ObjectPrinting/PrintingConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace ObjectPrinting -{ - public class PrintingConfig - { - public string PrintToString(TOwner obj) - { - 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; - - 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()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/AcceptanceTests.cs similarity index 53% rename from ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs rename to ObjectPrinting/Tests/AcceptanceTests.cs index 4c8b2445c..0557c6f3a 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/AcceptanceTests.cs @@ -1,27 +1,38 @@ -using NUnit.Framework; +using System; +using System.Globalization; +using NUnit.Framework; namespace ObjectPrinting.Tests { [TestFixture] - public class ObjectPrinterAcceptanceTests + public class AcceptanceTests { [Test] public void Demo() { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person {Id = Guid.NewGuid(), Name = "Alex", Age = 19, Height = 180}; - var printer = ObjectPrinter.For(); + var printer = ObjectPrinter.For() //1. Исключить из сериализации свойства определенного типа + .ExcludeType() //2. Указать альтернативный способ сериализации для определенного типа + .SerializeType().Use(prop => "Int Number: " + prop.ToString()) //3. Для числовых типов указать культуру + .SerializeType().Use(CultureInfo.InvariantCulture) //4. Настроить сериализацию конкретного свойства + .SerializeProperty(obj => obj.Height).Use(prop => "Person Height: " + prop.ToString()) //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .SerializeProperty(obj => obj.Name).TrimTo(1) //6. Исключить из сериализации конкретного свойства + .ExcludeProperty(obj => obj.Age); - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s1 = printer.PrintToString(person); + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); //8. ...с конфигурированием + var s3 = person.PrintToString(config => + config.ExcludeType() + .SerializeProperty(p => p.Name).TrimTo(2)); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/CollectionSerializationTests.cs b/ObjectPrinting/Tests/CollectionSerializationTests.cs new file mode 100644 index 000000000..7e6b2624a --- /dev/null +++ b/ObjectPrinting/Tests/CollectionSerializationTests.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CollectionSerializationTests : TestBase +{ + [Test] + public void PrintToString_WithList_SerializesListElements() + { + var list = CreateTestPeopleList(); + var result = list.PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("[0] = Person") + .And.Contain("[1] = Person") + .And.Contain("Name = First") + .And.Contain("Name = Second") + .And.Contain("Count = 2"); + } + + [Test] + public void PrintToString_WithArray_SerializesArrayElements() + { + var array = CreateTestStringArray(); + var result = array.PrintToString(); + + result.Should() + .Contain("String[]") + .And.Contain("[0] = apple") + .And.Contain("[1] = banana") + .And.Contain("[2] = cherry"); + } + + [Test] + public void PrintToString_WithDictionary_SerializesKeyValuePairs() + { + var dict = CreateTestDictionary(); + var result = dict.PrintToString(); + + result.Should() + .Contain("Dictionary`2") + .And.Contain("Key = first") + .And.Contain("Value = 1") + .And.Contain("Key = second") + .And.Contain("Value = 2") + .And.Contain("Key = third") + .And.Contain("Value = 3") + .And.Contain("Count = 3"); + } + + + [Test] + public void PrintToString_WithEmptyCollection_ShowsEmptyCollection() + { + var result = new List().PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("Count = 0"); + } + + [Test] + public void PrintToString_WithNestedCollections_SerializesRecursively() + { + var matrix = new List> + { + new List { 1, 2, 3 }, + new List { 4, 5, 6 } + }; + + var result = matrix.PrintToString(); + + result.Should() + .Contain("List`1") + .And.Contain("[0] = List`1") + .And.Contain("[1] = List`1") + .And.Contain("[0] = 1") + .And.Contain("[1] = 2") + .And.Contain("[2] = 3"); + } + + [Test] + public void + PrintToString_WithCollectionContainingNull_HandlesNullElements() + { + var listWithNull = new List { "first", null, "third" }; + var result = listWithNull.PrintToString(); + + result.Should() + .Contain("[0] = first") + .And.Contain("[1] = null") + .And.Contain("[2] = third"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ConfigurationSerializationTests.cs b/ObjectPrinting/Tests/ConfigurationSerializationTests.cs new file mode 100644 index 000000000..b03444d97 --- /dev/null +++ b/ObjectPrinting/Tests/ConfigurationSerializationTests.cs @@ -0,0 +1,184 @@ +using System; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ConfigurationSerializationTests : TestBase +{ + [Test] + public void PrintToString_WithExcludeType_ExcludesPropertiesOfType() + { + var printer = ObjectPrinter.For().ExcludeType(); + var result = printer.PrintToString(TestPerson); + + result.Should().NotContain("Id = ").And.Contain("Name = John Doe"); + } + + [Test] + public void PrintToString_WithExcludeProperty_ExcludesSpecificProperty() + { + var printer = ObjectPrinter.For().ExcludeProperty(p => p.Age); + var result = printer.PrintToString(TestPerson); + + result.Should().NotContain("Age = 30").And.Contain("Name = John Doe"); + } + + [Test] + public void PrintToString_WithTypeSerializer_UsesCustomSerialization() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(i => $"Integer: {i}"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Age = Integer: 30"); + } + + [Test] + public void PrintToString_WithCulture_UsesSpecifiedCulture() + { + var person = new Person { Height = 180.5 }; + var japaneseCulture = CultureInfo.GetCultureInfo("ja-JP"); + var printer = ObjectPrinter.For().SerializeType().Use(japaneseCulture); + var result = printer.PrintToString(person); + + result.Should().Contain("180.5"); + } + + [Test] + public void + PrintToString_WithPropertySerializer_UsesCustomSerializationForProperty() + { + var printer = ObjectPrinter.For() + .SerializeProperty(p => p.Name) + .Use(name => $"Name: {name.ToUpper()}"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Name = Name: JOHN DOE"); + } + + [Test] + public void PrintToString_WithStringPropertyTrim_TrimsStringProperty() + { + var printer = ObjectPrinter.For() + .SerializeProperty(p => p.Name).TrimTo(4); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Name = John"); + } + + [Test] + public void + PrintToString_WithMultipleConfigurations_AppliesAllConfigurations() + { + var printer = ObjectPrinter.For() + .ExcludeType() + .ExcludeProperty(p => p.Email) + .SerializeType().Use(i => $"{i} years") + .SerializeProperty(p => p.Name).TrimTo(4); + + var result = printer.PrintToString(TestPerson); + + result.Should() + .NotContain("Id = ") + .And.NotContain("Email = ") + .And.Contain("Age = 30 years") + .And.Contain("Name = John"); + } + + [Test] + public void PrintToString_WithCustomTypeSerializer_PriorityOverCulture() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(d => $"Height: {d} cm"); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("Height: 180,5 cm"); + } + + [Test] + public void + PrintToString_WithPropertySerializer_PriorityOverTypeSerializer() + { + var printer = ObjectPrinter.For() + .SerializeType().Use(s => s.ToUpper()) + .SerializeProperty(p => p.Name).Use(name => $"Mr. {name}"); + + var result = printer.PrintToString(TestPerson); + result.Should().Contain("Name = Mr. John Doe"); + } + + [Test] + public void PrintToString_WithInvariantCulture_FormatsNumbersConsistently() + { + var printer = ObjectPrinter.For().SerializeType() + .Use(CultureInfo.InvariantCulture); + var result = printer.PrintToString(TestPerson); + + result.Should().Contain("180.5"); + } + + [Test] + public void PrintingConfig_ExcludeMultipleTypes_WorksCorrectly() + { + var config = ObjectPrinter.For() + .ExcludeType() + .ExcludeType(); + + var person = new Person + { Id = Guid.NewGuid(), Name = "Test", Age = 25 }; + var result = config.PrintToString(person); + + result.Should() + .NotContain("Id = ") + .And.NotContain("Name = ") + .And.Contain("Age = 25"); + } + + [Test] + public void PrintingConfig_ChainedConfiguration_ReturnsCorrectType() + { + var config = ObjectPrinter.For() + .ExcludeType() + .SerializeType().Use(i => i.ToString()) + .SerializeProperty(p => p.Name).Use(n => n) + .ExcludeProperty(p => p.Age); + + config.Should().BeOfType>(); + } + + [Test] + public void PrintToString_WithConfigAndNestingLevel_AppliesBoth() + { + var nestedObject = new NestedContainer + { + Value = "Level 1", + Child = new NestedContainer + { + Value = "Very Long Value That Should Be Trimmed", + Child = new NestedContainer { Value = "Level 3" } + } + }; + + var result = nestedObject.PrintToString( + configurator => configurator + .SerializeProperty(x => x.Value).TrimTo(10), + nestingLevel: 2 + ); + + result.Should().Contain("Value = Level 1"); + result.Should().Contain("Value = Very Long"); + result.Should().NotContain("Level 3"); + } + + [Test] + public void PrintingConfig_ShouldReturnNewInstance_AfterConfiguration() + { + var printer = ObjectPrinter.For().ExcludeType(); + var printer2 = printer.ExcludeType(); + + printer.Should().NotBeSameAs(printer2); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/EdgeCasesTests.cs b/ObjectPrinting/Tests/EdgeCasesTests.cs new file mode 100644 index 000000000..5d9c2c8a2 --- /dev/null +++ b/ObjectPrinting/Tests/EdgeCasesTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class EdgeCasesTests : TestBase +{ + [Test] + public void PrintToString_WithCyclicReference_HandlesCyclicReference() + { + var result = PersonWithCyclicReference.PrintToString(); + + result.Should().Contain("Cyclic reference detected"); + Action action = () => PersonWithCyclicReference.PrintToString(); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithCyclicReferencesInCollections_DetectsCycles() + { + var list1 = new List(); + var list2 = new List { list1 }; + list1.Add(list2); + + var result = list1.PrintToString(); + result.Should().Contain("Cyclic reference detected"); + } + + + [Test] + public void PrintToString_WithZeroTrimLength_HandlesCorrectly() + { + var testClass = TestPerson; + var printer = ObjectPrinter.For() + .SerializeProperty(x => x.Name).TrimTo(0); + + var result = printer.PrintToString(testClass); + result.Should().Contain("Name = "); + } + + [Test] + public void PrintToString_WithComplexObjectGraph_DoesNotThrow() + { + var complexObject = new + { + Id = 1, + Name = "Test", + Items = new[] { "A", "B", "C" }, + Nested = new { Value = 42, Date = DateTime.Now } + }; + + Action action = () => complexObject.PrintToString(); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithFailingCustomSerializer_FallsBackToDefault() + { + var printer = ObjectPrinter.For() + .SerializeType() + .Use(s => throw new Exception("Test exception")); + + var person = new Person { Name = "Test" }; + Action action = () => printer.PrintToString(person); + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithSelfReference_DetectsCycle() + { + var selfReferencing = new Person(); + selfReferencing.Children.Add(selfReferencing); + + var result = selfReferencing.PrintToString(); + result.Should().Contain("Cyclic reference detected"); + } + + [Test] + public void PrintToString_WithNegativeNestingLevel_ThrowsArgumentException() + { + var person = new Person { Name = "Test" }; + + Action action = () => person.PrintToString(nestingLevel: -1); + + action.Should().Throw() + .WithMessage("Nesting level cannot be negative*"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs deleted file mode 100644 index f95559554..000000000 --- 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/ObjectPrinting/Tests/StandartSerializationTests.cs b/ObjectPrinting/Tests/StandartSerializationTests.cs new file mode 100644 index 000000000..4f23b511a --- /dev/null +++ b/ObjectPrinting/Tests/StandartSerializationTests.cs @@ -0,0 +1,157 @@ +using System; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class StandartSerializationTests : TestBase +{ + [Test] + public void PrintToString_SimpleObject_ReturnsCorrectFormat() + { + var result = TestPerson.PrintToString(); + + result.Should().NotBeNullOrEmpty(); + result.Should().Contain("Person"); + result.Should().Contain("Name = John Doe"); + result.Should().Contain("Age = 30"); + result.Should().Contain("Height = 180.5"); + } + + [Test] + public void PrintToString_WithNullProperty_HandlesNullCorrectly() + { + var person = new Person { Name = null }; + var result = person.PrintToString(); + + result.Should().Contain("Name = null"); + } + + [Test] + public void PrintToString_WithInheritance_IncludesAllProperties() + { + var testEmployee = new Employee + { + Name = "Jane", + Age = 25, + Position = "Developer", + Salary = 50000.50m + }; + + var result = testEmployee.PrintToString(); + + result.Should() + .Contain("Employee") + .And.Contain("Position = Developer") + .And.Contain("Salary = 50000.50") + .And.Contain("Name = Jane") + .And.Contain("Age = 25"); + } + + [Test] + public void PrintToString_ExtensionMethodWithoutConfig_Works() + { + var result = TestPerson.PrintToString(); + + result.Should().NotBeNullOrEmpty().And.Contain("Person"); + } + + [Test] + public void PrintToString_ExtensionMethodWithConfig_AppliesConfiguration() + { + var result = TestPerson.PrintToString(config => + config.ExcludeProperty(p => p.Age) + .SerializeProperty(p => p.Name).TrimTo(2)); + + result.Should().NotContain("Age = 30").And.Contain("Name = Jo"); + } + + [Test] + public void PrintToString_WithDateTime_SerializesCorrectly() + { + var testClass = new { Date = new DateTime(2023, 12, 31) }; + var result = testClass.PrintToString(); + + result.Should().Contain("Date = "); + } + + [Test] + public void PrintToString_WithEnum_SerializesCorrectly() + { + var testClass = new { Status = System.DateTimeKind.Utc }; + var result = testClass.PrintToString(); + + result.Should().Contain("Status = Utc"); + } + + [Test] + public void PrintToString_WithNestedObjects_SerializesRecursively() + { + var testCompany = new Company + { + Name = "Test Corp", + CEO = new Employee { Name = "CEO", Age = 45 } + }; + + var result = testCompany.PrintToString(); + + result.Should() + .Contain("Company") + .And.Contain("CEO = Employee") + .And.Contain("Name = CEO") + .And.Contain("Age = 45"); + } + + [Test] + public void PrintToString_WithDifferentNumericTypes_UsesCorrectFormatting() + { + var numericObject = new + { + IntValue = 42, + DoubleValue = 3.14159, + DecimalValue = 123.456m, + FloatValue = 2.718f + }; + + var result = numericObject.PrintToString(); + + result.Should() + .Contain("IntValue = 42") + .And.Contain("DoubleValue = 3.14159") + .And.Contain("DecimalValue = 123.456") + .And.Contain("FloatValue = 2.718"); + } + + [Test] + public void PrintToString_EmptyObject_ReturnsTypeName() + { + var result = new object().PrintToString(); + result.Should().Contain("Object"); + } + + [Test] + public void PrintToString_WithNestingLevelLimit_StopsAtSpecifiedDepth() + { + var nestedObject = new NestedContainer + { + Value = "Level 1", + Child = new NestedContainer + { + Value = "Level 2", + Child = new NestedContainer + { + Value = "Level 3", + Child = new NestedContainer { Value = "Level 4" } + } + } + }; + + var result = nestedObject.PrintToString(2); + + result.Should().Contain("Level 1"); + result.Should().Contain("Level 2"); + result.Should().NotContain("Level 3"); + result.Should().NotContain("Level 4"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestBaseClass.cs b/ObjectPrinting/Tests/TestBaseClass.cs new file mode 100644 index 000000000..7d1ad3689 --- /dev/null +++ b/ObjectPrinting/Tests/TestBaseClass.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +public class TestBase +{ + protected Person TestPerson { get; private set; } + protected Person PersonWithCyclicReference { get; private set; } + + [OneTimeSetUp] + public virtual void SetUp() + { + TestPerson = new Person + { + Id = Guid.NewGuid(), + Name = "John Doe", + Age = 30, + Height = 180.5, + Email = "john.doe@example.com" + }; + + var parent = new Person { Name = "Parent" }; + var child = new Person { Name = "Child", Parent = parent }; + parent.Children.Add(child); + PersonWithCyclicReference = parent; + } + + protected List CreateTestPeopleList() => new() + { + new Person { Name = "First", Age = 20 }, + new Person { Name = "Second", Age = 25 } + }; + + protected string[] CreateTestStringArray() => ["apple", "banana", "cherry"]; + + protected Dictionary CreateTestDictionary() => new() + { + ["first"] = 1, + ["second"] = 2, + ["third"] = 3 + }; +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/Company.cs b/ObjectPrinting/Tests/TestDataClasses/Company.cs new file mode 100644 index 000000000..f6583ad4a --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/Company.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class Company +{ + public string Name { get; set; } + public Employee CEO { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/Employee.cs b/ObjectPrinting/Tests/TestDataClasses/Employee.cs new file mode 100644 index 000000000..b73b23099 --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/Employee.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class Employee : Person +{ + public string Position { get; set; } + public decimal Salary { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs b/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs new file mode 100644 index 000000000..b263d7059 --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/NestedContainer.cs @@ -0,0 +1,7 @@ +namespace ObjectPrinting.Tests; + +public class NestedContainer +{ + public string Value { get; set; } + public NestedContainer Child { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/TestDataClasses/Person.cs b/ObjectPrinting/Tests/TestDataClasses/Person.cs new file mode 100644 index 000000000..022af032c --- /dev/null +++ b/ObjectPrinting/Tests/TestDataClasses/Person.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + + +namespace ObjectPrinting.Tests; + +public class Person +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public double Height { get; set; } + public string Email { get; set; } + public Person Parent { get; set; } + public List Children { get; set; } = []; +} \ No newline at end of file