diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index 3c7867c32..241b834eb 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -1,10 +1,226 @@ -namespace ObjectPrinting +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using ObjectPrinting.PrintingConfigs; + +namespace ObjectPrinting; + +public class ObjectPrinter { - public class ObjectPrinter + private readonly PrinterSettings settings; + + public ObjectPrinter(PrinterSettings settings) { - public static PrintingConfig For() + this.settings = settings; + } + + public static PrintingConfig For() + { + return new PrintingConfig(); + } + + public string PrintToString(TOwner obj) + { + return PrintToString(obj, 0, new HashSet()); + } + + private string PrintToString(object? obj, int nestingLevel, HashSet visited) + { + if (nestingLevel > settings.MaxRecursionDepth) + return string.Empty; + + if (obj is null) + return "null" + NewLine; + + var type = obj.GetType(); + + if (settings.ExcludedTypes.Contains(type)) + return string.Empty; + + if (IsCyclicReference(obj, type, visited)) + return "(cyclic reference)" + NewLine; + + return FormatByType(obj, nestingLevel, visited, type); + } + + private string FormatByType(object obj, int nestingLevel, HashSet visited, Type type) + { + if (TrySerializeType(type, obj, out var result)) + return result + NewLine; + + if (TryFormatFormattable(obj, type, out result)) + return result + NewLine; + + if (IsFinalType(type)) + return Convert.ToString(obj, CultureInfo.InvariantCulture) + NewLine; + + return obj switch + { + IDictionary dictionary => PrintDictionary(dictionary, nestingLevel, visited), + IEnumerable enumerable => PrintEnumerable(enumerable, nestingLevel, visited), + _ => PrintObject(obj, nestingLevel, visited) + }; + } + + private string PrintObject(object obj, int nestingLevel, HashSet visited) + { + var type = obj.GetType(); + var indent = Indent(nestingLevel + 1); + var sb = new StringBuilder(); + + sb.AppendLine(type.Name); + + var members = GetFilteredMembers(type); + + foreach (var member in members) + { + sb.Append(ProcessMember(obj, member, indent, nestingLevel, visited)); + } + + return sb.ToString(); + } + + private string ProcessMember( + object obj, MemberInfo member, string indent, int nestingLevel, HashSet visited) + { + var value = GetMemberValue(obj, member); + var prefix = $"{indent}{member.Name} = "; + + if (value is null) + return prefix + "null" + NewLine; + + if (settings.ExcludedTypes.Contains(value.GetType())) + return string.Empty; + + if (TrySerializeMember(member, value, out var result)) + return prefix + result + NewLine; + + if (TryTrimStringMember(member, value, out var trimmed)) + return prefix + trimmed + NewLine; + + return prefix + PrintToString(value, nestingLevel + 1, visited); + } + + private string PrintEnumerable(IEnumerable enumerable, int nestingLevel, HashSet visited) + { + var indent = Indent(nestingLevel + 1); + var sb = new StringBuilder(); + + sb.AppendLine("["); + + foreach (var item in enumerable) + { + sb.Append(indent).Append(PrintToString(item, nestingLevel + 1, visited)); + } + + sb.Append(Indent(nestingLevel)).AppendLine("]"); + return sb.ToString(); + } + + private string PrintDictionary(IDictionary dict, int nestingLevel, HashSet visited) + { + var indent = Indent(nestingLevel + 1); + var sb = new StringBuilder(); + + sb.AppendLine("{"); + + foreach (DictionaryEntry entry in dict) + { + sb.Append(indent) + .Append('[') + .Append(PrintToString(entry.Key, nestingLevel + 1, visited).TrimEnd()) + .Append("] = ") + .Append(PrintToString(entry.Value, nestingLevel + 1, visited)); + } + + sb.Append(Indent(nestingLevel)).AppendLine("}"); + return sb.ToString(); + } + + private bool TrySerializeType(Type type, object obj, out string? result) + { + if (settings.TypeSerializers.TryGetValue(type, out var serializer)) + { + result = serializer(obj); + return true; + } + + result = null; + return false; + } + + private bool TrySerializeMember(MemberInfo member, object value, out string? result) + { + if (settings.MemberSerializers.TryGetValue(member, out var serializer)) { - return new PrintingConfig(); + result = Convert.ToString(serializer(value), CultureInfo.InvariantCulture); + return true; } + + result = null; + return false; + } + + private bool TryFormatFormattable(object obj, Type type, out string? result) + { + if (obj is IFormattable formattable && settings.TypeCultures.TryGetValue(type, out var culture)) + { + result = Convert.ToString(formattable, culture); + return true; + } + + result = string.Empty; + return false; + } + + private bool TryTrimStringMember(MemberInfo member, object value, out string? result) + { + if (value is string stringValue && settings.TrimLengths.TryGetValue(member, out var trimLength)) + { + result = stringValue.Length > trimLength + ? stringValue.Substring(0, trimLength) + : stringValue; + return true; + } + + result = null; + return false; + } + + private static bool IsCyclicReference(object obj, Type type, HashSet visited) + { + return !type.IsValueType && !visited.Add(obj); + } + + private static bool IsFinalType(Type type) + { + return type.IsPrimitive + || type == typeof(string) + || type.IsEnum + || typeof(IFormattable).IsAssignableFrom(type); + } + + private IEnumerable GetFilteredMembers(Type type) + { + return type.GetProperties() + .Concat(type.GetFields().Cast()) + .Where(m => !settings.ExcludedMembers.Contains(m)); + } + + private static object? GetMemberValue(object? obj, MemberInfo member) + { + return member switch + { + PropertyInfo p => p.GetValue(obj), + FieldInfo f => f.GetValue(obj), + _ => throw new ArgumentOutOfRangeException($"Unsupported member type: {member.MemberType}") + }; } + + private static string Indent(int level) => new('\t', level); + private static string NewLine => Environment.NewLine; } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392ff..6a9fd13d2 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -3,10 +3,4 @@ net8.0 enable - - - - - - diff --git a/ObjectPrinting/PrinterSettings.cs b/ObjectPrinting/PrinterSettings.cs new file mode 100644 index 000000000..7196ead80 --- /dev/null +++ b/ObjectPrinting/PrinterSettings.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace ObjectPrinting; + +public class PrinterSettings +{ + public int MaxRecursionDepth { get; } + public IReadOnlySet ExcludedTypes { get; } + public IReadOnlySet ExcludedMembers { get; } + public IReadOnlyDictionary> TypeSerializers { get; } + public IReadOnlyDictionary> MemberSerializers { get; } + public IReadOnlyDictionary TypeCultures { get; } + public IReadOnlyDictionary TrimLengths { get; } + + public PrinterSettings( + int maxRecursionDepth, + IReadOnlySet excludedTypes, + IReadOnlySet excludedMembers, + IReadOnlyDictionary> typeSerializers, + IReadOnlyDictionary> memberSerializers, + IReadOnlyDictionary typeCultures, + IReadOnlyDictionary trimLengths) + { + MaxRecursionDepth = maxRecursionDepth; + ExcludedTypes = excludedTypes; + ExcludedMembers = excludedMembers; + TypeSerializers = typeSerializers; + MemberSerializers = memberSerializers; + TypeCultures = typeCultures; + TrimLengths = trimLengths; + } +} \ No newline at end of file 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/PrintingConfigs/Extensions/ObjectExtensions.cs b/ObjectPrinting/PrintingConfigs/Extensions/ObjectExtensions.cs new file mode 100644 index 000000000..152d0798a --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/Extensions/ObjectExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace ObjectPrinting.PrintingConfigs.Extensions; + +public static class ObjectExtensions +{ + public static string Print(this T obj) + { + return ObjectPrinter.For().Create().PrintToString(obj); + } + + public static string Print(this T obj, Func, PrintingConfig> config) + { + return config(ObjectPrinter.For()).Create().PrintToString(obj); + } +} + diff --git a/ObjectPrinting/PrintingConfigs/Extensions/TypePrintingConfigExtensions.cs b/ObjectPrinting/PrintingConfigs/Extensions/TypePrintingConfigExtensions.cs new file mode 100644 index 000000000..691e8a472 --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/Extensions/TypePrintingConfigExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting.PrintingConfigs.Extensions; + +public static class TypePrintingConfigExtensions +{ + public static PrintingConfig Using( + this TypePrintingConfig config, CultureInfo cultureInfo) + where TProp : IFormattable + { + return config.Config.SetCultureFor(cultureInfo); + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfigs/PrintingConfig.cs b/ObjectPrinting/PrintingConfigs/PrintingConfig.cs new file mode 100644 index 000000000..deba632f5 --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/PrintingConfig.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting.PrintingConfigs; + +public class PrintingConfig +{ + private int maxRecursionDepth = 16; + private readonly HashSet excludedTypes = new(); + private readonly HashSet excludedMembers = new(); + private readonly Dictionary> typeSerializers = new(); + private readonly Dictionary> memberSerializers = new(); + private readonly Dictionary typeCultures = new(); + private readonly Dictionary trimLengths = new(); + + public PrintingConfig SetMaxRecursionDepth(int recursionDepth) + { + if (recursionDepth < 0) + { + throw new ArgumentException("Max recursion depth must be greater than zero."); + } + maxRecursionDepth = recursionDepth; + return this; + } + + public PrintingConfig Excluding() + { + excludedTypes.Add(typeof(TProp)); + return this; + } + + public PrintingConfig Excluding(Expression> selector) + { + excludedMembers.Add(GetMember(selector)); + return this; + } + + public TypePrintingConfig Printing() + { + return new TypePrintingConfig(this); + } + + public PropertyPrintingConfig Printing(Expression> selector) + { + return new PropertyPrintingConfig(this, selector); + } + + public StringPropertyPrintingConfig Printing(Expression> selector) + { + return new StringPropertyPrintingConfig(this, selector); + } + + public PrintingConfig SetSerializerFor(Func serializer) + { + typeSerializers[typeof(TProp)] = p => serializer((TProp)p); + return this; + } + + public PrintingConfig SetSerializerFor( + Expression> selector, Func serializer) + { + memberSerializers[GetMember(selector)] = m => serializer((TProp)m); + return this; + } + + public PrintingConfig SetCultureFor(CultureInfo culture) + where TProp : IFormattable + { + typeCultures[typeof(TProp)] = culture; + return this; + } + + public PrintingConfig TrimMember(Expression> selector, int trimLength) + { + if (trimLength < 0) + { + throw new ArgumentException("Max length cannot be less than zero."); + } + + trimLengths[GetMember(selector)] = trimLength; + return this; + } + + private PrinterSettings CreateSettings() + { + return new PrinterSettings( + maxRecursionDepth, + new HashSet(excludedTypes), + new HashSet(excludedMembers), + new Dictionary>(typeSerializers), + new Dictionary>(memberSerializers), + new Dictionary(typeCultures), + new Dictionary(trimLengths) + ); + } + + public ObjectPrinter Create() + { + return new ObjectPrinter(CreateSettings()); + } + + private static MemberInfo GetMember(Expression> selector) + { + if (selector.Body is MemberExpression memberExpression) + { + return memberExpression.Member; + } + + throw new ArgumentException("Selector must refer to a property or a field."); + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfigs/PropertyPrintingConfig.cs b/ObjectPrinting/PrintingConfigs/PropertyPrintingConfig.cs new file mode 100644 index 000000000..878710c83 --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/PropertyPrintingConfig.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting.PrintingConfigs; + +public class PropertyPrintingConfig +{ + protected readonly PrintingConfig Config; + protected readonly Expression> Selector; + + public PropertyPrintingConfig(PrintingConfig config, Expression> selector) + { + Config = config; + Selector = selector; + } + + public PrintingConfig Using(Func serializer) + { + return Config.SetSerializerFor(Selector, serializer); + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfigs/StringPropertyPrintingConfig.cs b/ObjectPrinting/PrintingConfigs/StringPropertyPrintingConfig.cs new file mode 100644 index 000000000..0134fce74 --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/StringPropertyPrintingConfig.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq.Expressions; + +namespace ObjectPrinting.PrintingConfigs; + +public class StringPropertyPrintingConfig : PropertyPrintingConfig +{ + public StringPropertyPrintingConfig( + PrintingConfig config, Expression> selector) + : base(config, selector) + { + } + + public PrintingConfig TrimToLength(int maxLength) + { + return Config.TrimMember(Selector, maxLength); + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfigs/TypePrintingConfig.cs b/ObjectPrinting/PrintingConfigs/TypePrintingConfig.cs new file mode 100644 index 000000000..2b6fcfbc5 --- /dev/null +++ b/ObjectPrinting/PrintingConfigs/TypePrintingConfig.cs @@ -0,0 +1,18 @@ +using System; + +namespace ObjectPrinting.PrintingConfigs; + +public class TypePrintingConfig +{ + internal readonly PrintingConfig Config; + + internal TypePrintingConfig(PrintingConfig config) + { + Config = config; + } + + public PrintingConfig Using(Func serializer) + { + return Config.SetSerializerFor(serializer); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectExtensions.cs b/ObjectPrinting/Solved/ObjectExtensions.cs deleted file mode 100644 index b0c94553c..000000000 --- a/ObjectPrinting/Solved/ObjectExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - 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/Solved/ObjectPrinter.cs b/ObjectPrinting/Solved/ObjectPrinter.cs deleted file mode 100644 index 540ee769c..000000000 --- a/ObjectPrinting/Solved/ObjectPrinter.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting.Solved -{ - public class ObjectPrinter - { - public static PrintingConfig For() - { - return new PrintingConfig(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/ObjectPrinting/Solved/PrintingConfig.cs deleted file mode 100644 index 0ec5aeb2b..000000000 --- a/ObjectPrinting/Solved/PrintingConfig.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Text; - -namespace ObjectPrinting.Solved -{ - public class PrintingConfig - { - public PropertyPrintingConfig Printing() - { - return new PropertyPrintingConfig(this); - } - - public PropertyPrintingConfig Printing(Expression> memberSelector) - { - return new PropertyPrintingConfig(this); - } - - public PrintingConfig Excluding(Expression> memberSelector) - { - return this; - } - - internal PrintingConfig Excluding() - { - return this; - } - - 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/Solved/PropertyPrintingConfig.cs b/ObjectPrinting/Solved/PropertyPrintingConfig.cs deleted file mode 100644 index a509697d1..000000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfig.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Globalization; - -namespace ObjectPrinting.Solved -{ - public class PropertyPrintingConfig : IPropertyPrintingConfig - { - private readonly PrintingConfig printingConfig; - - public PropertyPrintingConfig(PrintingConfig printingConfig) - { - this.printingConfig = printingConfig; - } - - public PrintingConfig Using(Func print) - { - return printingConfig; - } - - public PrintingConfig Using(CultureInfo culture) - { - return printingConfig; - } - - PrintingConfig IPropertyPrintingConfig.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 deleted file mode 100644 index dd3922394..000000000 --- a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved -{ - public static class PropertyPrintingConfigExtensions - { - public static string PrintToString(this T obj, Func, PrintingConfig> config) - { - return config(ObjectPrinter.For()).PrintToString(obj); - } - - public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propConfig, int maxLen) - { - return ((IPropertyPrintingConfig)propConfig).ParentConfig; - } - - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index ac52d5ee5..000000000 --- a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Globalization; -using NUnit.Framework; - -namespace ObjectPrinting.Solved.Tests -{ - [TestFixture] - public class ObjectPrinterAcceptanceTests - { - [Test] - public void Demo() - { - var person = new Person { Name = "Alex", Age = 19 }; - - var printer = ObjectPrinter.For() - //1. Исключить из сериализации свойства определенного типа - .Excluding() - //2. Указать альтернативный способ сериализации для определенного типа - .Printing().Using(i => i.ToString("X")) - //3. Для числовых типов указать культуру - .Printing().Using(CultureInfo.InvariantCulture) - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - .Printing(p => p.Name).TrimmedToLength(10) - //6. Исключить из сериализации конкретного свойства - .Excluding(p => p.Age); - - string s1 = printer.PrintToString(person); - - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - string s2 = person.PrintToString(); - - //8. ...с конфигурированием - string s3 = person.PrintToString(s => s.Excluding(p => p.Age)); - Console.WriteLine(s1); - Console.WriteLine(s2); - Console.WriteLine(s3); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/Solved/Tests/Person.cs b/ObjectPrinting/Solved/Tests/Person.cs deleted file mode 100644 index 858ebbf8d..000000000 --- a/ObjectPrinting/Solved/Tests/Person.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ObjectPrinting.Solved.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/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs deleted file mode 100644 index 4c8b2445c..000000000 --- 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/Tests/GlobalUsings.cs b/Tests/GlobalUsings.cs new file mode 100644 index 000000000..cefced496 --- /dev/null +++ b/Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Tests/TestEntities/CollectionTestsClass.cs b/Tests/TestEntities/CollectionTestsClass.cs new file mode 100644 index 000000000..e194cc26e --- /dev/null +++ b/Tests/TestEntities/CollectionTestsClass.cs @@ -0,0 +1,8 @@ +namespace Tests.TestEntities; + +public class CollectionTestsClass +{ + public string Name { get; set; } = null!; + public int Age { get; set; } + public List Friends { get; set; } = new(); +} \ No newline at end of file diff --git a/Tests/TestEntities/CultureTestClass.cs b/Tests/TestEntities/CultureTestClass.cs new file mode 100644 index 000000000..1afe2acfd --- /dev/null +++ b/Tests/TestEntities/CultureTestClass.cs @@ -0,0 +1,8 @@ +namespace Tests.TestEntities; + +public class CultureTestClass +{ + public decimal Decimal { get; set; } + public DateTime Date { get; set; } + public double Double { get; set; } +} diff --git a/Tests/TestEntities/CustomSerializerTestsClass.cs b/Tests/TestEntities/CustomSerializerTestsClass.cs new file mode 100644 index 000000000..8a881a0b2 --- /dev/null +++ b/Tests/TestEntities/CustomSerializerTestsClass.cs @@ -0,0 +1,10 @@ +namespace Tests.TestEntities; + +public class CustomSerializerTestsClass +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public int Age { get; set; } + public decimal Price { get; set; } + public decimal Discount { get; set; } +} \ No newline at end of file diff --git a/Tests/TestEntities/Departmnet.cs b/Tests/TestEntities/Departmnet.cs new file mode 100644 index 000000000..ef4f32a17 --- /dev/null +++ b/Tests/TestEntities/Departmnet.cs @@ -0,0 +1,6 @@ +namespace Tests.TestEntities; + +public class Department +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/Tests/TestEntities/Node.cs b/Tests/TestEntities/Node.cs new file mode 100644 index 000000000..8371e6db6 --- /dev/null +++ b/Tests/TestEntities/Node.cs @@ -0,0 +1,30 @@ +namespace Tests.TestEntities; + +public class Node +{ + public string Value { get; set; } = null!; + public Node? Next { get; set; } + + public static Node CreateChain(int length) + { + var parent = new Node { Value = "level0" }; + var node = parent; + + for (int i = 0; i < length; i++) + { + var nextNode = new Node { Value = $"level{i + 1}"}; + node.Next = nextNode; + node = nextNode; + } + return parent; + } + + public static Node CreateChainWithCycle() + { + var node1 = new Node { Value = "0" }; + var node2 = new Node { Value = "1" }; + node1.Next = node2; + node2.Next = node1; + return node1; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/Tests/TestEntities/Person.cs similarity index 63% rename from ObjectPrinting/Tests/Person.cs rename to Tests/TestEntities/Person.cs index f95559554..f2226102b 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/Tests/TestEntities/Person.cs @@ -1,11 +1,9 @@ -using System; - -namespace ObjectPrinting.Tests +namespace Tests.TestEntities { public class Person { public Guid Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } = null!; public double Height { get; set; } public int Age { get; set; } } diff --git a/Tests/TestEntities/SuperComplexClass.cs b/Tests/TestEntities/SuperComplexClass.cs new file mode 100644 index 000000000..066b13586 --- /dev/null +++ b/Tests/TestEntities/SuperComplexClass.cs @@ -0,0 +1,19 @@ +namespace Tests.TestEntities; + +public class SuperComplexClass +{ + public string ShortName { get; set; } = null!; + public string VeryLongDescription { get; set; } = null!; + public int Age { get; set; } + public double Weight { get; set; } + public decimal Salary { get; set; } + public decimal Bonus { get; set; } + public DateTime BirthDate { get; set; } + public Guid UserId { get; set; } + public bool IsActive { get; set; } + public List Tags { get; set; } = new(); + public Dictionary Scores { get; set; } = new(); + public SuperComplexClass Manager { get; set; } + public SuperComplexClass Assistant { get; set; } + public List TeamMembers { get; set; } = new(); +} \ No newline at end of file diff --git a/Tests/TestEntities/TestClass.cs b/Tests/TestEntities/TestClass.cs new file mode 100644 index 000000000..faca63909 --- /dev/null +++ b/Tests/TestEntities/TestClass.cs @@ -0,0 +1,8 @@ +namespace Tests.TestEntities; + + +public class TestClass +{ + public string Property { get; set; } = null!; + public string field; +} diff --git a/Tests/TestEntities/TrimTestsClass.cs b/Tests/TestEntities/TrimTestsClass.cs new file mode 100644 index 000000000..7fc139ad3 --- /dev/null +++ b/Tests/TestEntities/TrimTestsClass.cs @@ -0,0 +1,8 @@ +namespace Tests.TestEntities; + +public class TrimTestsClass +{ + public string Short { get; set; } = null!; + public string Long { get; set; } = null!; + public string Description { get; set; } = null!; +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 000000000..4876d048d --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/Tests/Tests/ObjectPrinterAcceptanceTests.cs b/Tests/Tests/ObjectPrinterAcceptanceTests.cs new file mode 100644 index 000000000..9d997bed4 --- /dev/null +++ b/Tests/Tests/ObjectPrinterAcceptanceTests.cs @@ -0,0 +1,34 @@ +using System.Globalization; +using ObjectPrinting; +using ObjectPrinting.PrintingConfigs.Extensions; +using Tests.TestEntities; + +namespace Tests.Tests +{ + [TestFixture] + public class ObjectPrinterAcceptanceTests + { + [Test] + public void Demo() + { + var person = new Person { Name = "Alex", Age = 19 }; + + var printer = ObjectPrinter.For() + .Excluding() // 1. Исключить из сериализации свойства определенного типа + .Printing().Using(i => $"INT: {i}") // 2. Указать альтернативный способ сериализации для определенного типа + .Printing().Using(CultureInfo.InvariantCulture) // 3. Для числовых типов указать культуру + .Printing(p => p.Age).Using(a => $"AGE : {a}") // 4. Настроить сериализацию конкретного свойства + .Printing(p => p.Name).TrimToLength(10) // 5. Настроить обрезание строковых свойств + .Excluding(p => p.Age) // 6. Исключить конкретное свойство + .Create(); + + var s1 = printer.PrintToString(person); + + var s2 = person.Print(); // 7. Синтаксический сахар — метод расширения + + var s3 = person.Print(p => p + .Printing().Using(CultureInfo.GetCultureInfo("ru-RU")) // 8. ...с конфигурированием + ); + } + } +} \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.CollectionTests.cs b/Tests/Tests/ObjectPrintingTests.CollectionTests.cs new file mode 100644 index 000000000..7bef2692a --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.CollectionTests.cs @@ -0,0 +1,184 @@ + using System.Collections; + using System.Collections.ObjectModel; + using FluentAssertions; + using ObjectPrinting; + using Tests.TestEntities; + + namespace Tests.Tests; + + public partial class ObjectPrintingTests + { + [TestFixture] + public class CollectionTests + { + [Test] + public void PrintToString_Array_ShouldPrintEachElement() + { + var data = new[] { 1, 2, 3 }; + var printer = ObjectPrinter.For().Create(); + var result = printer.PrintToString(data); + + var expected = $""" + [ + {'\t'}1 + {'\t'}2 + {'\t'}3 + ] + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_ArrayOfObjects_ShouldSerializeEachElement() + { + var data = new[] + { + new CollectionTestsClass { Name = "Alice", Age = 25 }, + new CollectionTestsClass { Name = "Bob", Age = 30 } + }; + var printer = ObjectPrinter.For().Create(); + var result = printer.PrintToString(data); + + var expected = $""" + [ + {'\t'}CollectionTestsClass + {'\t'}{'\t'}Name = Alice + {'\t'}{'\t'}Age = 25 + {'\t'}{'\t'}Friends = [ + {'\t'}{'\t'}] + {'\t'}CollectionTestsClass + {'\t'}{'\t'}Name = Bob + {'\t'}{'\t'}Age = 30 + {'\t'}{'\t'}Friends = [ + {'\t'}{'\t'}] + ] + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_Dictionary_ShouldPrintKeysAndValues() + { + var dict = new Dictionary + { + [1] = "1", + [2] = "2" + }; + var printer = ObjectPrinter.For>().Create(); + var result = printer.PrintToString(dict); + + var expected = $$""" + { + {{'\t'}}[1] = 1 + {{'\t'}}[2] = 2 + } + + """; + + result.Should().Be(expected); + } + + + [Test] + public void PrintToString_DictionaryWithComplexObjects_ShouldSerializeKeysAndValues() + { + var dict = new Dictionary + { + [new CollectionTestsClass { Name = "Manager" }] = new() { Name = "HR" }, + [new CollectionTestsClass { Name = "Developer" }] = new() { Name = "IT" } + }; + + var printer = ObjectPrinter + .For>() + .Create(); + var result = printer.PrintToString(dict); + + var expected = $$""" + { + {{'\t'}}[CollectionTestsClass + {{'\t'}}{{'\t'}}Name = Manager + {{'\t'}}{{'\t'}}Age = 0 + {{'\t'}}{{'\t'}}Friends = [ + {{'\t'}}{{'\t'}}]] = Department + {{'\t'}}{{'\t'}}Name = HR + {{'\t'}}[CollectionTestsClass + {{'\t'}}{{'\t'}}Name = Developer + {{'\t'}}{{'\t'}}Age = 0 + {{'\t'}}{{'\t'}}Friends = [ + {{'\t'}}{{'\t'}}]] = Department + {{'\t'}}{{'\t'}}Name = IT + } + + """; + + result.Should().Be(expected); + } + + + [Test] + public void PrintToString_NullCollection_ShouldShowNull() + { + var data = new CollectionTestsClass { Name = "Test", Friends = null }; + var printer = ObjectPrinter + .For() + .Create(); + var result = printer.PrintToString(data); + + var expected = $""" + CollectionTestsClass + {'\t'}Name = Test + {'\t'}Age = 0 + {'\t'}Friends = null + + """; + + + result.Should().Be(expected); + } + + private static IEnumerable EmptyCollectionsTestCases + { + get + { + yield return new TestCaseData(Array.Empty()) + .SetName("EmptyArray_ShouldShowEmptyArrayStructure"); + + yield return new TestCaseData(new List()) + .SetName("EmptyList_ShouldShowEmptyListStructure"); + + yield return new TestCaseData(new HashSet()) + .SetName("EmptyHashSet_ShouldShowEmptyHashSetStructure"); + + yield return new TestCaseData(new Collection()) + .SetName("EmptyCollection_ShouldShowEmptyCollectionStructure"); + } + } + + [Test] + [TestCaseSource(nameof(EmptyCollectionsTestCases))] + public void PrintToString_EmptyCollections_ShouldShowEmptyStructure( + IEnumerable emptyCollection) + { + var printer = ObjectPrinter.For().Create(); + var result = printer.PrintToString(emptyCollection); + + var expected = $"[{Environment.NewLine}]{Environment.NewLine}"; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_EmptyDictionary_ShouldShowEmptyStructure() + { + var printer = ObjectPrinter.For>().Create(); + var result = printer.PrintToString(new Dictionary()); + + var expected = $"{{{Environment.NewLine}}}{Environment.NewLine}"; + result.Should().Be(expected); + } + } + } \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.CultureTests.cs b/Tests/Tests/ObjectPrintingTests.CultureTests.cs new file mode 100644 index 000000000..b4602c9e8 --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.CultureTests.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using FluentAssertions; +using ObjectPrinting; +using ObjectPrinting.PrintingConfigs.Extensions; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [TestFixture] + public class CultureTests + { + private CultureTestClass testData; + + [SetUp] + public void SetUp() + { + testData = new CultureTestClass + { + Decimal = 1234.56m, + Date = new DateTime(2023, 10, 15, 14, 30, 0), + Double = 1234.567 + }; + } + + [Test] + public void PrintToString_CustomCulture_ShouldApplyFormatting() + { + var printer = ObjectPrinter.For() + .Printing().Using(new CultureInfo("de-DE")) + .Create(); + var result = printer.PrintToString(testData); + + var expected = $""" + CultureTestClass + {'\t'}Decimal = 1234,56 + {'\t'}Date = 10/15/2023 14:30:00 + {'\t'}Double = 1234.567 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_MultipleCultures_ShouldApplyAll() + { + var printer = ObjectPrinter.For() + .Printing().Using(new CultureInfo("es-ES")) + .Printing().Using(new CultureInfo("en-US")) + .Printing().Using(new CultureInfo("fr-FR")) + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CultureTestClass + {'\t'}Decimal = 1234.56 + {'\t'}Date = 15/10/2023 14:30:00 + {'\t'}Double = 1234,567 + + """; + + result.Should().Be(expected); + } + } +} \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.CustomSerializersTests.cs b/Tests/Tests/ObjectPrintingTests.CustomSerializersTests.cs new file mode 100644 index 000000000..e39864e89 --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.CustomSerializersTests.cs @@ -0,0 +1,192 @@ +using System.Globalization; +using FluentAssertions; +using ObjectPrinting; +using ObjectPrinting.PrintingConfigs.Extensions; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [TestFixture] + public class CustomSerializerTests + { + private CustomSerializerTestsClass testData; + + [SetUp] + public void SetUp() + { + testData = new CustomSerializerTestsClass + { + Id = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"), + Name = "John", + Age = 25, + Price = 19.99m, + Discount = 0.1m + }; + } + + [Test] + public void PrintToString_CustomTypeSerializer_ShouldFormatType() + { + var printer = ObjectPrinter.For() + .Printing().Using(g => g + .ToString("N").Substring(0, 8).ToUpper()) + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = A1B2C3D4 + {'\t'}Name = John + {'\t'}Age = 25 + {'\t'}Price = 19.99 + {'\t'}Discount = 0.1 + + """; + + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_CustomPropertySerializer_ShouldOverrideTypeSerializer() + { + var printer = ObjectPrinter.For() + .Printing().Using(i => $"{i} years") + .Printing(p => p.Age).Using(age => $"***{age}***") + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4-e5f6-7890-abcd-ef1234567890 + {'\t'}Name = John + {'\t'}Age = ***25*** + {'\t'}Price = 19.99 + {'\t'}Discount = 0.1 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_CustomSerializerReturnsNull_ShouldHandleCorrectly() + { + var printer = ObjectPrinter.For() + .Printing(p => p.Name).Using(_ => null) + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4-e5f6-7890-abcd-ef1234567890 + {'\t'}Name = + {'\t'}Age = 25 + {'\t'}Price = 19.99 + {'\t'}Discount = 0.1 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_CustomSerializerForMultipleTypes_ShouldApplyAll() + { + var printer = ObjectPrinter.For() + .Printing().Using(g => g.ToString().Substring(0, 8)) + .Printing().Using(i => $"#{i}") + .Printing().Using(p => $"${p}") + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4 + {'\t'}Name = John + {'\t'}Age = #25 + {'\t'}Price = $19,99 + {'\t'}Discount = $0,1 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_CustomSerializerForOneProperty_ShouldNotAffectOtherProperties() + { + var printer = ObjectPrinter.For() + .Printing(p => p.Price).Using(p => $"${p}") + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4-e5f6-7890-abcd-ef1234567890 + {'\t'}Name = John + {'\t'}Age = 25 + {'\t'}Price = $19,99 + {'\t'}Discount = 0.1 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_MultiplePropertySerializers_ShouldApplyEachToRespectiveProperty() + { + var printer = ObjectPrinter.For() + .Printing(p => p.Name).Using(n => $"{n.ToUpper()}") + .Printing(p => p.Age).Using(a => $"{a} years") + .Printing(p => p.Price).Using(p => $"${p}") + .Create(); + + var result = printer.PrintToString(testData); + + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4-e5f6-7890-abcd-ef1234567890 + {'\t'}Name = JOHN + {'\t'}Age = 25 years + {'\t'}Price = $19,99 + {'\t'}Discount = 0.1 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_CustomSerializerDominatesOverCulture() + { + var printer = ObjectPrinter.For() + .Printing().Using(new CultureInfo("de-DE")) + .Printing().Using(p => $"${p:0.00}") + .Create(); + + var result = printer.PrintToString(testData); + + var expected = $""" + CustomSerializerTestsClass + {'\t'}Id = a1b2c3d4-e5f6-7890-abcd-ef1234567890 + {'\t'}Name = John + {'\t'}Age = 25 + {'\t'}Price = $19,99 + {'\t'}Discount = $0,10 + + """; + + result.Should().Be(expected); + } + } +} \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.ExcludingTests.cs b/Tests/Tests/ObjectPrintingTests.ExcludingTests.cs new file mode 100644 index 000000000..d9687d11a --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.ExcludingTests.cs @@ -0,0 +1,145 @@ +using FluentAssertions; +using ObjectPrinting; +using ObjectPrinting.PrintingConfigs; +using ObjectPrinting.PrintingConfigs.Extensions; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [TestFixture] + public class ExcludingTests + { + private Person person; + + [SetUp] + public void SetUp() + { + person = new Person { Name = "Alex", Age = 19, Height = 175.123 }; + } + + + [Test] + public void PrintToString_ExcludedType_ShouldNotSerializeMembersOfThatType() + { + var printer = new PrintingConfig() + .Excluding() + .Create(); + + var result = printer.PrintToString(person); + + var expected = $""" + Person + {'\t'}Id = 00000000-0000-0000-0000-000000000000 + {'\t'}Name = Alex + {'\t'}Age = 19 + + """; + + result.Should().Be(expected); + } + + + [Test] + public void PrintToString_ExcludedProperty_ShouldNotSerializeProperty() + { + var printer = new PrintingConfig() + .Excluding(p => p.Age) + .Create(); + + var result = printer.PrintToString(person); + + var expected = $""" + Person + {'\t'}Id = 00000000-0000-0000-0000-000000000000 + {'\t'}Name = Alex + {'\t'}Height = 175.123 + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_ExcludedField_ShouldNotSerializeField() + { + var obj = new TestClass { Property = "Prop", field = "Field" }; + + var result = obj.Print(c => c.Excluding(o => o.field)); + + var expected = $""" + TestClass + {'\t'}Property = Prop + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_ExcludedMultipleMembers_ShouldNotSerializeMultipleMembers() + { + var printer = ObjectPrinter.For() + .Excluding(p => p.Id) + .Excluding(p => p.Age) + .Create(); + + var result = printer.PrintToString(person); + var expected = $""" + Person + {'\t'}Name = Alex + {'\t'}Height = 175.123 + + """; + + result.Should().Be(expected); + } + + [Test] + public void Excluding_WhenNotMemberProvided_ShouldThrowArgumentException() + { + var printer = ObjectPrinter.For(); + + var act = () => printer.Excluding(p => p.GetHashCode()); + + act.Should().Throw(); + } + + [Test] + public void PrintToString_AllPropertiesExcluded_ShouldShowTypeName() + { + var printer = ObjectPrinter.For() + .Excluding() + .Excluding() + .Excluding(p => p.Id) + .Excluding(p => p.Height) + .Create(); + + var result = printer.PrintToString(person); + + result.Should().Be("Person" + Environment.NewLine); + } + + [Test] + public void PrintToString_Excluding_ShouldDominateOnPrinting() + { + var printer = ObjectPrinter.For() + .Excluding(p => p.Id) + .Printing(p => p.Id).Using(p => "id") + .Create(); + var result = printer.PrintToString(person); + + var expected = $""" + Person + {'\t'}Name = Alex + {'\t'}Height = 175.123 + {'\t'}Age = 19 + + """; + + result.Should().Be(expected); + + } + } +} \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.NestedObjectsTest.cs b/Tests/Tests/ObjectPrintingTests.NestedObjectsTest.cs new file mode 100644 index 000000000..5c176b9da --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.NestedObjectsTest.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using ObjectPrinting; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [TestFixture] + public class NestedObjectsTest + { + [Test] + public void PrintToString_NestedObject_ShouldPrintAllLevels() + { + var node = Node.CreateChain(3); + var printer = ObjectPrinter + .For() + .Create(); + + var result = printer.PrintToString(node); + + result.Should().Contain("level0") + .And.Contain("level1") + .And.Contain("level2"); + } + + [Test] + public void PrintToString_WithCyclicReference_ShouldNotFall() + { + var node = Node.CreateChainWithCycle(); + var printer = ObjectPrinter + .For() + .Create(); + + var act = () => printer.PrintToString(node); + + act.Should().NotThrow(); + act().Should().Contain("cyclic reference"); + } + + [Test] + public void PrintToString_WhenReachesMaxRecursionLevel_ShouldStop() + { + var node = Node.CreateChain(16); + var printer = ObjectPrinter + .For() + .Create(); + + var result = printer.PrintToString(node); + + result.Should() + .Contain("level15") + .And.NotContain("level16"); + } + + [Test] + public void SetMaxRecursionDepth_NegativeValue_ShouldThrowArgumentException() + { + var printer = ObjectPrinter.For(); + + var act = () => printer.SetMaxRecursionDepth(-1); + + act.Should().Throw(); + } + + } +} \ No newline at end of file diff --git a/Tests/Tests/ObjectPrintingTests.PrintToString_SimpleObject_ShouldSerializeAllMembers.verified.txt b/Tests/Tests/ObjectPrintingTests.PrintToString_SimpleObject_ShouldSerializeAllMembers.verified.txt new file mode 100644 index 000000000..47ca6bd8a --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.PrintToString_SimpleObject_ShouldSerializeAllMembers.verified.txt @@ -0,0 +1,5 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = John + Height = 0 + Age = 25 diff --git a/Tests/Tests/ObjectPrintingTests.PrintToString_WithMaximumConfigurations_ShouldApplyAllCorrectly.verified.txt b/Tests/Tests/ObjectPrintingTests.PrintToString_WithMaximumConfigurations_ShouldApplyAllCorrectly.verified.txt new file mode 100644 index 000000000..735020a40 --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.PrintToString_WithMaximumConfigurations_ShouldApplyAllCorrectly.verified.txt @@ -0,0 +1,48 @@ +SuperComplexClass + ShortName = Jo + VeryLongDescription = This is a very long + Age = Age is 35 + Salary = Salary: 75000,50$ + BirthDate = 1988-05-15 + Tags = Tags[4] + Scores = Scores[3] + Manager = SuperComplexClass + ShortName = Ma + VeryLongDescription = null + Age = Age is 45 + Salary = Salary: 0$ + BirthDate = 0001-01-01 + Tags = Tags[0] + Scores = Scores[0] + Manager = null + Assistant = null + TeamMembers = [ + (cyclic reference) + ] + Assistant = SuperComplexClass + ShortName = Al + VeryLongDescription = null + Age = Age is 28 + Salary = Salary: 60000,00$ + BirthDate = 0001-01-01 + Tags = Tags[0] + Scores = Scores[0] + Manager = null + Assistant = null + TeamMembers = [ + ] + TeamMembers = [ + (cyclic reference) + SuperComplexClass + ShortName = Bo + VeryLongDescription = null + Age = Age is 32 + Salary = Salary: 65000,00$ + BirthDate = 0001-01-01 + Tags = Tags[0] + Scores = Scores[0] + Manager = null + Assistant = null + TeamMembers = [ + ] + ] diff --git a/Tests/Tests/ObjectPrintingTests.TrimTests.cs b/Tests/Tests/ObjectPrintingTests.TrimTests.cs new file mode 100644 index 000000000..858662ca0 --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.TrimTests.cs @@ -0,0 +1,110 @@ +using FluentAssertions; +using ObjectPrinting; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [TestFixture] + public class TrimTests + { + [Test] + public void PrintToString_TrimToZeroLength_ShouldReturnEmptyString() + { + var data = new TrimTestsClass { Long = "Very long text" }; + var printer = ObjectPrinter.For() + .Printing(p => p.Long).TrimToLength(0) + .Create(); + var result = printer.PrintToString(data); + + var expected = $""" + TrimTestsClass + {'\t'}Short = null + {'\t'}Long = + {'\t'}Description = null + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_TrimMultipleStringProperties_ShouldTrimEachIndependently() + { + var data = new TrimTestsClass + { + Short = "Short", + Long = "Very long text here", + Description = "Medium description" + }; + var printer = ObjectPrinter.For() + .Printing(p => p.Short).TrimToLength(3) + .Printing(p => p.Long).TrimToLength(10) + .Printing(p => p.Description).TrimToLength(6) + .Create(); + var result = printer.PrintToString(data); + + var expected = $""" + TrimTestsClass + {'\t'}Short = Sho + {'\t'}Long = Very long + {'\t'}Description = Medium + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_TrimToLengthGreaterThanString_ShouldReturnOriginalString() + { + var data = new TrimTestsClass { Short = "Hi" }; + var printer = ObjectPrinter.For() + .Printing(p => p.Short).TrimToLength(10) + .Create(); + var result = printer.PrintToString(data); + + var expected = $""" + TrimTestsClass + {'\t'}Short = Hi + {'\t'}Long = null + {'\t'}Description = null + + """; + + result.Should().Be(expected); + } + + [Test] + public void PrintToString_TrimNullString_ShouldHandleGracefully() + { + var data = new TrimTestsClass { Short = null }; + var printer = ObjectPrinter.For() + .Printing(p => p.Short).TrimToLength(5) + .Create(); + var result = printer.PrintToString(data); + + var expected = $""" + TrimTestsClass + {'\t'}Short = null + {'\t'}Long = null + {'\t'}Description = null + + """; + + result.Should().Be(expected); + } + + [Test] + public void TrimToLength_NotPositiveValue_ShouldThrowArgumentException() + { + var printer = ObjectPrinter.For(); + + var act = () => printer.Printing(p => p.Short).TrimToLength(-1); + + act.Should().Throw(); + } + } +} + diff --git a/Tests/Tests/ObjectPrintingTests.cs b/Tests/Tests/ObjectPrintingTests.cs new file mode 100644 index 000000000..744fef64b --- /dev/null +++ b/Tests/Tests/ObjectPrintingTests.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using ObjectPrinting; +using ObjectPrinting.PrintingConfigs.Extensions; +using Tests.TestEntities; + +namespace Tests.Tests; + +public partial class ObjectPrintingTests +{ + [Test] + public async Task PrintToString_SimpleObject_ShouldSerializeAllMembers() + { + var person = new Person { Name = "John", Age = 25 }; + var result = person.Print(); + + await Verify(result); + } + + [Test] + public async Task PrintToString_WithMaximumConfigurations_ShouldApplyAllCorrectly() + { + var testData = new SuperComplexClass + { + ShortName = "John", + VeryLongDescription = "This is a very long description that should be trimmed because it exceeds the maximum allowed length for this property", + Age = 35, + Weight = 75.5, + Salary = 75000.50m, + Bonus = 15000.25m, + BirthDate = new DateTime(1988, 5, 15, 10, 30, 0), + UserId = Guid.Parse("12345678-1234-1234-1234-123456789012"), + IsActive = true, + Tags = new List { "developer", "senior", "backend", "very-long-tag-that-should-be-trimmed" }, + Scores = new Dictionary + { + ["C#"] = 95, + ["SQL"] = 88, + ["JavaScript"] = 76 + }, + TeamMembers = new List + { + new() { ShortName = "Alice", Age = 28, Salary = 60000.00m }, + new() { ShortName = "Bob", Age = 32, Salary = 65000.00m } + }, + Manager = new SuperComplexClass { ShortName = "Manager", Age = 45 } + }; + + testData.Manager.TeamMembers.Add(testData); + testData.Assistant = testData.TeamMembers[0]; + + var printer = ObjectPrinter.For() + .Excluding() + .Excluding() + .Excluding(x => x.Weight) + .Excluding(x => x.Bonus) + .Printing().Using(i => $"{i} years") + .Printing().Using(d => $"{d} kg") + .Printing().Using(d => $"{d:C2}") + .Printing().Using(dt => dt.ToString("yyyy-MM-dd")) + .Printing().Using(s => s.ToUpper()) + .Printing(x => x.Salary).Using(s => $"Salary: {s}$") + .Printing(x => x.Age).Using(a => $"Age is {a}") + .Printing(x => x.ShortName).TrimToLength(2) + .Printing(x => x.VeryLongDescription).TrimToLength(20) + .Printing().Using(new CultureInfo("de-DE")) + .Printing().Using(new CultureInfo("fr-FR")) + .Printing>().Using(list => $"Tags[{list.Count}]") + .Printing>().Using(dict => $"Scores[{dict.Count}]") + .Create(); + + var result = printer.PrintToString(testData); + + await Verify(result); + } +} \ No newline at end of file diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9ed..4992958ad 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}") = "Tests", "Tests\Tests.csproj", "{CF73988E-264C-4C79-9422-298155B0F249}" +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 + {CF73988E-264C-4C79-9422-298155B0F249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF73988E-264C-4C79-9422-298155B0F249}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF73988E-264C-4C79-9422-298155B0F249}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF73988E-264C-4C79-9422-298155B0F249}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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