From ea0a5ebb85bd02a25519c3d76033d4e067645049 Mon Sep 17 00:00:00 2001 From: s1lenter Date: Sat, 22 Nov 2025 16:32:07 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A0=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/IPropertyPrintingConfig.cs | 8 + ObjectPrinting/ITypePrintingConfig.cs | 8 + ObjectPrinting/ObjectPrinter.cs | 11 +- ObjectPrinting/ObjectPrinterExtensions.cs | 11 + ObjectPrinting/PrintingConfig.cs | 234 +++++++++-- ObjectPrinting/PropertyPrintingConfig.cs | 17 + .../PropertyPrintingConfigExtensions.cs | 11 + ...ernativePropertySerialization.verified.txt | 9 + ..._AlternativeTypeSerialization.verified.txt | 9 + ...intToString_CircularReference.verified.txt | 9 + ...PrintToString_ExcludeProperty.verified.txt | 8 + ...sts.PrintToString_ExcludeType.verified.txt | 8 + ....PrintToString_SerializeArray.verified.txt | 19 + ...tToString_SerializeDictionary.verified.txt | 23 ++ ...s.PrintToString_SerializeList.verified.txt | 19 + ...oString_SetCultureForProperty.verified.txt | 9 + ...intToString_SetCultureForType.verified.txt | 9 + ...interTests.PrintToString_Trim.verified.txt | 9 + ObjectPrinting/TypePrintingConfig.cs | 16 + ObjectPrintingTests/Family.cs | 8 + .../ObjectPrinterAcceptanceTests.cs | 68 ++++ .../ObjectPrintingTests.csproj | 28 ++ ObjectPrintingTests/Person.cs | 13 + ObjectPrintingTests/PrintingConfigTests.cs | 374 ++++++++++++++++++ fluent-api.sln | 6 + 25 files changed, 913 insertions(+), 31 deletions(-) create mode 100644 ObjectPrinting/IPropertyPrintingConfig.cs create mode 100644 ObjectPrinting/ITypePrintingConfig.cs create mode 100644 ObjectPrinting/ObjectPrinterExtensions.cs create mode 100644 ObjectPrinting/PropertyPrintingConfig.cs create mode 100644 ObjectPrinting/PropertyPrintingConfigExtensions.cs create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt create mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt create mode 100644 ObjectPrinting/TypePrintingConfig.cs create mode 100644 ObjectPrintingTests/Family.cs create mode 100644 ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs create mode 100644 ObjectPrintingTests/ObjectPrintingTests.csproj create mode 100644 ObjectPrintingTests/Person.cs create mode 100644 ObjectPrintingTests/PrintingConfigTests.cs diff --git a/ObjectPrinting/IPropertyPrintingConfig.cs b/ObjectPrinting/IPropertyPrintingConfig.cs new file mode 100644 index 000000000..08037fed1 --- /dev/null +++ b/ObjectPrinting/IPropertyPrintingConfig.cs @@ -0,0 +1,8 @@ +using System; + +namespace ObjectPrinting; + +public interface IPropertyPrintingConfig +{ + public PrintingConfig Using(Func func); +} \ No newline at end of file diff --git a/ObjectPrinting/ITypePrintingConfig.cs b/ObjectPrinting/ITypePrintingConfig.cs new file mode 100644 index 000000000..b10f07881 --- /dev/null +++ b/ObjectPrinting/ITypePrintingConfig.cs @@ -0,0 +1,8 @@ +using System; + +namespace ObjectPrinting; + +public interface ITypePrintingConfig +{ + public PrintingConfig Using(Func func); +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index 3c7867c32..b2ce90a79 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -1,10 +1,9 @@ -namespace ObjectPrinting +namespace ObjectPrinting; + +public class ObjectPrinter { - public class ObjectPrinter + public static PrintingConfig For() { - public static PrintingConfig For() - { - return new PrintingConfig(); - } + return new PrintingConfig(); } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterExtensions.cs b/ObjectPrinting/ObjectPrinterExtensions.cs new file mode 100644 index 000000000..88c4d583f --- /dev/null +++ b/ObjectPrinting/ObjectPrinterExtensions.cs @@ -0,0 +1,11 @@ +using System; + +namespace ObjectPrinting; + +public static class ObjectPrinterExtensions +{ + public static string PrintToString(this T obj) => ObjectPrinter.For().PrintToString(obj); + + public static string PrintToString(this T obj, Func, PrintingConfig> config) => + config(ObjectPrinter.For()).PrintToString(obj); +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e082117..04adfd05c 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,225 @@ using System; -using System.Linq; +using System.Collections; +using System.Collections.Generic; using System.Text; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting; -namespace ObjectPrinting +public class PrintingConfig { - public class PrintingConfig + private readonly HashSet typesToExclude = new(ReferenceEqualityComparer.Instance); + private readonly HashSet propertiesToExclude = []; + private readonly HashSet processedObjects = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary cultureSerializers = []; + private readonly Dictionary> typeSerializers = []; + private readonly Dictionary> propertySerializers = []; + private int MaxNestingLevel { get; set; } = 5; + + public PrintingConfig SetMaxNestingLevel(int maxNestingLevel) + { + if (maxNestingLevel < 0) + { + throw new ArgumentException("Max nesting level must be greater than or equal to 0."); + } + + MaxNestingLevel = maxNestingLevel; + return this; + } + + public PrintingConfig Exclude() + { + typesToExclude.Add(typeof(TPropType)); + return this; + } + + public PrintingConfig Exclude(Expression> propertySelector) + { + propertiesToExclude.Add(GetPropertyMemberInfo(propertySelector).Name); + return this; + } + + public IPropertyPrintingConfig PrintPropertySettings( + Expression> propertySelector) + { + return new PropertyPrintingConfig(this, GetPropertyMemberInfo(propertySelector).Name); + } + + public ITypePrintingConfig PrintSettings() + { + return new TypePrintingConfig(this); + } + + public PrintingConfig UseCulture(CultureInfo culture) where TPropType : IFormattable + { + cultureSerializers[typeof(TPropType)] = culture; + return this; + } + + internal void AddTypeSerializer(Func serializeFunc) + { + if (typeSerializers.TryGetValue(typeof(TPropType), out var serializers)) + { + serializers.Add(serializeFunc); + return; + } + typeSerializers[typeof(TPropType)] = [serializeFunc]; + } + + internal void AddPropertySerializer(string propertyName, Func serializeFunc) + { + if (propertySerializers.TryGetValue(propertyName, out var serializers)) + { + serializers.Add(serializeFunc); + return; + } + propertySerializers[propertyName] = [serializeFunc]; + } + + public string PrintToString(TOwner obj) + { + return PrintToString(obj, 1); + } + + private string PrintToString(object? obj, int nestingLevel) + { + if (obj is null) + { + return "null"; + } + + if (processedObjects.Contains(obj)) + { + return "It is not possible to print an object with a circular reference."; + } + + var type = obj.GetType(); + + if (typeSerializers.TryGetValue(type, out var serializers)) + { + var result = serializers.Aggregate(obj, (current, serializer) => serializer.DynamicInvoke(current)!); + + return result.ToString()!; + } + + if (type.IsPrimitive || type == typeof(string) || type == typeof(Guid)) + { + return type == typeof(string) ? $"\"{obj}\"" : obj.ToString()!; + } + + if (nestingLevel > MaxNestingLevel) + { + return $"The maximum recursion depth has been reached: {MaxNestingLevel}."; + } + + processedObjects.Add(obj); + if (obj is IEnumerable enumerable && type != typeof(string)) + { + processedObjects.Clear(); + return SerializeEnumerable(enumerable, nestingLevel); + } + + var indentation = new string('\t', nestingLevel); + var stringBuilderResult = new StringBuilder(); + stringBuilderResult.AppendLine($"{type.Name}"); + + foreach (var propertyInfo in type.GetProperties()) + { + if (propertiesToExclude.Contains(propertyInfo.Name) || typesToExclude.Contains(propertyInfo.PropertyType)) + { + continue; + } + + var propertyValue = propertyInfo.GetValue(obj); + var propertyType = propertyInfo.PropertyType; + var propertyName = propertyInfo.Name; + + stringBuilderResult.AppendLine(GetSerializeString(nestingLevel, propertyName, indentation, propertyValue, + propertyType)); + } + + return stringBuilderResult.ToString(); + } + + private string GetSerializeString( + int nestingLevel, + string propertyName, + string indentation, + object? propertyValue, + Type propertyType + ) { - public string PrintToString(TOwner obj) + if (propertySerializers.TryGetValue(propertyName, out var serializers)) { - return PrintToString(obj, 0); + var result = + serializers.Aggregate(propertyValue, (current, serializer) => serializer.DynamicInvoke(current)); + + return $"{indentation}{propertyName} = {result!}"; } - private string PrintToString(object obj, int nestingLevel) + if (cultureSerializers.TryGetValue(propertyType, out var culture)) { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + var propertyValueFormattable = propertyValue as IFormattable; + + return $"{indentation}{propertyName} = {propertyValueFormattable!.ToString(null, culture)}"; + } + + return $"{indentation}{propertyName} = {PrintToString(propertyValue, nestingLevel + 1)}"; + } + + private string SerializeEnumerable(IEnumerable enumerable, int nestingLevel) + { + var objects = enumerable.Cast().ToList(); + + if (objects.Count == 0) + { + return "{}"; + } + + var indentation = new string('\t', nestingLevel); + var serializeResult = new StringBuilder(); + serializeResult.AppendLine("{"); - var finalTypes = new[] + if (enumerable is IDictionary dictionary) + { + foreach (var key in dictionary.Keys) { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + var value = dictionary[key]; + var keyConvert = PrintToString(key, nestingLevel); + var valueConvert = PrintToString(value, nestingLevel + 1); + + serializeResult.Append($"{indentation}{keyConvert}: {valueConvert}" + Environment.NewLine); + } + } + else + { + for (var i = 0; i < objects.Count; i++) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + var obj = objects[i]; + serializeResult.Append($"{indentation}{PrintToString(obj, nestingLevel + 1)}"); + if (i < objects.Count - 1) + { + serializeResult.Append($"{indentation},"); + serializeResult.Append(Environment.NewLine); + } } - return sb.ToString(); } + + serializeResult.AppendLine($"{indentation}}}"); + + return serializeResult.ToString(); + } + + private static MemberInfo GetPropertyMemberInfo(Expression> propertySelector) + { + return propertySelector.Body switch + { + MemberExpression memberExpression => memberExpression.Member, + UnaryExpression { Operand: MemberExpression operand } => operand.Member, + var _ => throw new ArgumentException("Invalid property selector expression") + }; } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 000000000..f26b05530 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,17 @@ +using System; + +namespace ObjectPrinting; + +public class PropertyPrintingConfig(PrintingConfig config, string propertyNameName) + : IPropertyPrintingConfig +{ + private PrintingConfig Config { get; } = config; + private string PropertyName { get; } = propertyNameName; + + public PrintingConfig Using(Func func) + { + Config.AddPropertySerializer(PropertyName, func); + + return Config; + } +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs new file mode 100644 index 000000000..0b5e8ef88 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,11 @@ +namespace ObjectPrinting; + +public static class PropertyPrintingConfigExtensions +{ + public static PrintingConfig TrimmedTo( + this IPropertyPrintingConfig config, + int length) + { + return config.Using(str => str == null ? "null" : str[..length]); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt new file mode 100644 index 000000000..eca958436 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = NoName + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt new file mode 100644 index 000000000..f0a7c193b --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = date + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt new file mode 100644 index 000000000..5b7723d5c --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = Circular Reference diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt new file mode 100644 index 000000000..8226d3285 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt @@ -0,0 +1,8 @@ +Person + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt new file mode 100644 index 000000000..94d0a1581 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt @@ -0,0 +1,8 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt new file mode 100644 index 000000000..3feff8971 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt @@ -0,0 +1,19 @@ +Person[]: + Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null + Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bread + Height = 222 + Weight = 111 + Age = 22 + Birthday = 11/11/2001 00:00:00 + IsStudent = False + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt new file mode 100644 index 000000000..9797b02d8 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt @@ -0,0 +1,23 @@ +Dictionary`2: + KeyValuePair`2 + Key = Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null + Value = first + KeyValuePair`2 + Key = Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bread + Height = 222 + Weight = 111 + Age = 22 + Birthday = 11/11/2001 00:00:00 + IsStudent = False + Father = null + Value = second diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt new file mode 100644 index 000000000..498a0de81 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt @@ -0,0 +1,19 @@ +List`1: + Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null + Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bread + Height = 222 + Weight = 111 + Age = 22 + Birthday = 11/11/2001 00:00:00 + IsStudent = False + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt new file mode 100644 index 000000000..9091c50c5 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200,2 + Weight = 100.11 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt new file mode 100644 index 000000000..878e54b14 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bob + Height = 200,2 + Weight = 100,11 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt new file mode 100644 index 000000000..0a320f449 --- /dev/null +++ b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt @@ -0,0 +1,9 @@ +Person + Id = 00000000-0000-0000-0000-000000000000 + Name = Bo + Height = 200 + Weight = 100 + Age = 20 + Birthday = 10/10/2000 00:00:00 + IsStudent = True + Father = null diff --git a/ObjectPrinting/TypePrintingConfig.cs b/ObjectPrinting/TypePrintingConfig.cs new file mode 100644 index 000000000..e9c141dcf --- /dev/null +++ b/ObjectPrinting/TypePrintingConfig.cs @@ -0,0 +1,16 @@ +using System; + +namespace ObjectPrinting; + +public class TypePrintingConfig(PrintingConfig config) + : ITypePrintingConfig +{ + private PrintingConfig Config { get; } = config; + + public PrintingConfig Using(Func func) + { + Config.AddTypeSerializer(func); + + return Config; + } +} \ No newline at end of file diff --git a/ObjectPrintingTests/Family.cs b/ObjectPrintingTests/Family.cs new file mode 100644 index 000000000..c8a42600b --- /dev/null +++ b/ObjectPrintingTests/Family.cs @@ -0,0 +1,8 @@ +namespace ObjectPrinting.Test; + +public record Family +{ + public Person? Mom { get; set; } + public Person? Dad { get; set; } + public List Children { get; set; } = []; +} \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs b/ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs new file mode 100644 index 000000000..6320cb05e --- /dev/null +++ b/ObjectPrintingTests/ObjectPrinterAcceptanceTests.cs @@ -0,0 +1,68 @@ +using System.Globalization; + +namespace ObjectPrinting.Test; + +[TestFixture] +public class ObjectPrinterAcceptanceTests +{ + [Test] + public void Demo() + { + var person = new Person + { + Name = "Alex", + Age = 19, + BestFriend = new Person + { + Name = "Bob", + Age = 40, + BestFriend = new Person() + }, + Friends = + [ + new Person + { + Name = "Alice", + Age = 19, + }, + new Person + { + Name = "Robert", + Age = 19, + }, + ], + BodyParts = + { + { "Hand", 2 }, + { "Foot", 2 }, + { "Head", 1 }, + { "Tail", 0 } + } + }; + + var printer = ObjectPrinter.For() + //1. Исключить из сериализации свойства определенного типа + .Exclude() + //2. Указать альтернативный способ сериализации для определенного типа + .PrintSettings().Using(i => i.ToString("X")) + //3. Для числовых типов указать культуру + .UseCulture(CultureInfo.InvariantCulture) + //4. Настроить сериализацию конкретного свойства + .PrintPropertySettings(x => x.Surname).Using(p => $"-{p}-") + //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .PrintPropertySettings(p => p.Name).TrimmedTo(2) + //6. Исключить из сериализации конкретного свойства + .Exclude(p => p.Age); + + var s1 = printer.PrintToString(person); + Console.WriteLine(s1); + + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); + Console.WriteLine(s2); + + //8. ...с конфигурированием + var s3 = person.PrintToString(p => p.Exclude(x => x.Name)); + Console.WriteLine(s3); + } +} \ No newline at end of file diff --git a/ObjectPrintingTests/ObjectPrintingTests.csproj b/ObjectPrintingTests/ObjectPrintingTests.csproj new file mode 100644 index 000000000..e5b8d2681 --- /dev/null +++ b/ObjectPrintingTests/ObjectPrintingTests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/ObjectPrintingTests/Person.cs b/ObjectPrintingTests/Person.cs new file mode 100644 index 000000000..aa0336c4d --- /dev/null +++ b/ObjectPrintingTests/Person.cs @@ -0,0 +1,13 @@ +namespace ObjectPrinting.Test; + +public record Person +{ + public Guid Id { get; set; } + public string? Name { get; set; } + public string? Surname { get; set; } + public double Height { get; set; } + public int Age { get; set; } + public Person? BestFriend { get; set; } + public Person[] Friends { get; set; } = []; + public Dictionary BodyParts { get; set; } = new(); +} \ No newline at end of file diff --git a/ObjectPrintingTests/PrintingConfigTests.cs b/ObjectPrintingTests/PrintingConfigTests.cs new file mode 100644 index 000000000..df74958a8 --- /dev/null +++ b/ObjectPrintingTests/PrintingConfigTests.cs @@ -0,0 +1,374 @@ +using System.Globalization; +using System.Text; +using FluentAssertions; + +namespace ObjectPrinting.Test; + +public class TestsObjectPrinting +{ + private Person firstPerson; + private Person secondPerson; + private Family family; + + [SetUp] + public void Setup() + { + firstPerson = new() + { + Name = "Ben", + Surname = "Big", + Height = 170.1, + Age = 20, + BestFriend = new() { Name = "Bob", Surname = "Boby", Height = 40, Age = 80 }, + Friends = + [ + new() { Name = "Alice", Surname = "Sev", Height = 50, Age = 30 }, + new() { Name = "Max", Surname = "Albor", Height = 10, Age = 9 } + ], + BodyParts = { { "Hand", 2 }, { "Foot", 2 }, { "Head", 1 }, { "Tail", 0 } } + }; + + secondPerson = new(); + + family = new() { Mom = firstPerson, Dad = secondPerson, Children = [firstPerson, secondPerson] }; + } + + [Test] + public void Exclude_ExcludeType() + { + const string unexpectedName = nameof(Person.Name); + const string unexpectedSurname = nameof(Person.Surname); + var result = ObjectPrinter.For() + .Exclude() + .PrintToString(firstPerson); + + result.Should().NotContain(unexpectedName).And.NotContain(unexpectedSurname); + } + + [Test] + public void Exclude_ExcludeProperty() + { + const string unexpectedAge = nameof(Person.Age); + var result = ObjectPrinter.For() + .Exclude(p => p.Age) + .PrintToString(firstPerson); + + result.Should().NotContain(unexpectedAge); + } + + [Test] + public void Exclude_AddSerializationToType_ThenExcludeType() + { + var unexpectedName = $"{firstPerson.Name} is beautiful"; + var unexpectedSurname = $"{firstPerson.Surname} is beautiful"; + var result = ObjectPrinter.For() + .PrintSettings().Using(p => $"{p} is beautiful") + .Exclude() + .PrintToString(firstPerson); + + result.Should().NotContain(unexpectedName).And.NotContain(unexpectedSurname); + } + + [Test] + public void Exclude_ExcludeAllProperties() + { + const string expected = "Person\r\n"; + var result = ObjectPrinter.For() + .Exclude(p => p.Id) + .Exclude(p => p.Name) + .Exclude(p => p.Surname) + .Exclude(p => p.Height) + .Exclude(p => p.Age) + .Exclude(p => p.BestFriend) + .Exclude(p => p.Friends) + .Exclude(p => p.BodyParts) + .PrintToString(firstPerson); + + result.Should().Be(expected); + } + + [Test] + public void Exclude_ExcludeAllTypes() + { + const string expected = "Person\r\n"; + var result = ObjectPrinter.For() + .Exclude() + .Exclude() + .Exclude() + .Exclude() + .Exclude() + .Exclude() + .Exclude>() + .PrintToString(firstPerson); + + result.Should().Be(expected); + } + + [Test] + public void Using_SerializationForString() + { + const string expected = " is beautiful"; + var result = ObjectPrinter.For() + .PrintSettings() + .Using(p => $"{p} is beautiful") + .PrintToString(firstPerson); + + result.Should().Contain(expected); + } + + [Test] + public void Using_SerializationForInt() + { + const string exceptedAge = $"{nameof(firstPerson.Age)} = 10"; + var result = ObjectPrinter.For() + .PrintSettings() + .Using(_ => "10") + .PrintToString(firstPerson); + + result.Should().Contain(exceptedAge); + } + + [Test] + public void Using_SerializationForName() + { + var expected = firstPerson.Name?.ToUpper(); + var unexpected = firstPerson.Name; + var result = ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .Using(p => p!.ToUpper()) + .PrintToString(firstPerson); + + result.Should().Contain(expected).And.NotContain(unexpected); + } + + [Test] + public void Using_MultiplePropertySerializations() + { + var exceptedAge = $"{nameof(firstPerson.Name)} = 21{firstPerson.Name}12"; + var result = ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .Using(name => $"1{name}1") + .PrintPropertySettings(p => p.Name) + .Using(name => $"2{name}2") + .PrintToString(firstPerson); + + result.Should().Contain(exceptedAge); + } + + [Test] + public void Using_MultipleTypeSerializationsForString() + { + var exceptedName = $"{nameof(firstPerson.Name)} = 21{firstPerson.Name}12"; + var result = ObjectPrinter.For() + .PrintSettings() + .Using(str => $"1{str}1") + .PrintSettings() + .Using(str => $"2{str}2") + .PrintToString(firstPerson); + + result.Should().Contain(exceptedName); + } + + [Test] + public void Using_TrimmedToPropertiesAfterSerializationForName([Values(0, 1, 2)] int length) + { + var exceptedName = $"{nameof(firstPerson.Name)} = " + $"1{firstPerson.Name!}"[..length]; + var result = ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .Using(name => $"1{name}1") + .PrintPropertySettings(p => p.Name) + .TrimmedTo(length) + .PrintToString(firstPerson); + + result.Should().Contain(exceptedName); + } + + [Test] + public void UseCulture_ChangeCultureForDouble() + { + var expected = firstPerson.Height.ToString(new CultureInfo("ru-RU")); + var unexpected = firstPerson.Height.ToString(new CultureInfo("en-US")); + var result = ObjectPrinter.For() + .UseCulture(new("ru-RU")) + .PrintToString(firstPerson); + + result.Should().Contain(expected).And.NotContain(unexpected); + } + + [Test] + public void TrimmedTo_TrimmingName([Values(0, 1, 2)] int length) + { + var expected = $"{nameof(firstPerson.Name)} = {firstPerson.Name?[..length]}"; + var unexpected = firstPerson.Name; + var result = ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .TrimmedTo(length) + .PrintToString(firstPerson); + + result.Should().Contain(expected).And.NotContain(unexpected); + } + + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void SetMaxNestingLevel_WithDifferentNestingLevels(int levelNesting) + { + firstPerson.BestFriend = secondPerson; + var expected = $"The maximum recursion depth has been reached: {levelNesting}."; + var result = ObjectPrinter.For() + .SetMaxNestingLevel(levelNesting) + .PrintToString(firstPerson); + + result.Should().Contain(expected); + } + + [TestCase(-1)] + [TestCase(-2)] + [TestCase(-3)] + [TestCase(-4)] + [TestCase(-5)] + public void SetMaxNestingLevel_WithNegativeNestingLevel(int levelNesting) + { + FluentActions.Invoking(() => ObjectPrinter.For() + .SetMaxNestingLevel(levelNesting)) + .Should().Throw(); + } + + [Test] + public void PrintToString_ProcessingCyclicLinksBetweenObjects() + { + firstPerson.BestFriend = secondPerson; + secondPerson.BestFriend = firstPerson; + + var result = ObjectPrinter.For() + .PrintToString(firstPerson); + result.Should().Contain("It is not possible to print an object with a circular reference."); + } + + [Test] + public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_Array() + { + var result = ObjectPrinter.For() + .PrintToString([firstPerson, firstPerson]); + + result.Should().NotContain("It is not possible to print an object with a circular reference."); + } + + [Test] + public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_List() + { + var result = ObjectPrinter.For>() + .PrintToString([firstPerson, firstPerson]); + + result.Should().NotContain("It is not possible to print an object with a circular reference."); + } + + [Test] + public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_Dictionary() + { + var result = ObjectPrinter.For>() + .PrintToString(new() { { firstPerson, firstPerson } }); + + result.Should().NotContain("It is not possible to print an object with a circular reference."); + } + + [Test] + public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_InsideObject() + { + family = family with { Mom = firstPerson, Dad = firstPerson }; + var result = ObjectPrinter.For() + .PrintToString(family); + + result.Should().NotContain("It is not possible to print an object with a circular reference."); + } + + [Test] + public void PrintToString_CorrectStructure() + { + var expectedResult = new StringBuilder(); + expectedResult.Append("Person\r\n\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t" + + "Name = \"Ben\"\r\n\t" + + "Surname = \"Big\"\r\n\t" + + "Height = 170,1\r\n\t" + + "Age = 20\r\n\t" + + "BestFriend = Person\r\n\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t" + + "Name = \"Bob\"\r\n\t\t" + + "Surname = \"Boby\"\r\n\t\t" + + "Height = 40\r\n\t\t" + + "Age = 80\r\n\t\t" + + "BestFriend = null\r\n\t\t" + + "Friends = {}\r\n\t\t" + + "BodyParts = {}\r\n\r\n\t" + + "Friends = {\r\n\t\t" + + "Person\r\n\t\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + + "Name = \"Alice\"\r\n\t\t\t" + + "Surname = \"Sev\"\r\n\t\t\t" + + "Height = 50\r\n\t\t\t" + + "Age = 30\r\n\t\t\t" + + "BestFriend = null\r\n\t\t\t" + + "Friends = {}\r\n\t\t\t" + + "BodyParts = {}\r\n\t\t,\r\n\t\t" + + "Person\r\n\t\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + + "Name = \"Max\"\r\n\t\t\t" + + "Surname = \"Albor\"\r\n\t\t\t" + + "Height = 10\r\n\t\t\t" + + "Age = 9\r\n\t\t\t" + + "BestFriend = null\r\n\t\t\t" + + "Friends = {}\r\n\t\t\t" + + "BodyParts = {}\r\n\t\t" + + "}\r\n\r\n\t" + + "BodyParts = {\r\n\t\t\"Hand\": 2\r\n\t\t\"Foot\": 2\r\n\t\t\"Head\": 1\r\n\t\t\"Tail\": 0\r\n\t\t}\r\n\r\n"); + + var result = expectedResult.ToString(); + var v2 = ObjectPrinter.For() + .PrintToString(firstPerson); + + result.Should().Contain(v2); + } + + [Test] + public void PrintToString_WithExcludedProperiesStructure() + { + var expectedResult = new StringBuilder(); + expectedResult.Append("Person\r\n\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t" + + "Surname = \"Big\"\r\n\t" + + "Height = 170,1\r\n\t" + + "BestFriend = Person\r\n\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t" + + "Surname = \"Boby\"\r\n\t\t" + + "Height = 40\r\n\t\t" + + "BestFriend = null\r\n\t\t" + + "Friends = {}\r\n\t\t" + + "BodyParts = {}\r\n\r\n\t" + + "Friends = {\r\n\t\t" + + "Person\r\n\t\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + + "Surname = \"Sev\"\r\n\t\t\t" + + "Height = 50\r\n\t\t\t" + + "BestFriend = null\r\n\t\t\t" + + "Friends = {}\r\n\t\t\t" + + "BodyParts = {}\r\n\t\t,\r\n\t\t" + + "Person\r\n\t\t\t" + + "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + + "Surname = \"Albor\"\r\n\t\t\t" + + "Height = 10\r\n\t\t\t" + + "BestFriend = null\r\n\t\t\t" + + "Friends = {}\r\n\t\t\t" + + "BodyParts = {}\r\n\t\t" + + "}\r\n\r\n\t" + + "BodyParts = {\r\n\t\t\"Hand\": 2\r\n\t\t\"Foot\": 2\r\n\t\t\"Head\": 1\r\n\t\t\"Tail\": 0\r\n\t\t}\r\n\r\n"); + + var result = expectedResult.ToString(); + var v2 = ObjectPrinter.For() + .Exclude(p => p.Name) + .Exclude(p => p.Age) + .PrintToString(firstPerson); + + result.Should().Contain(v2); + } +} \ No newline at end of file diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9ed..c09e2207a 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}") = "ObjectPrintingTests", "ObjectPrintingTests\ObjectPrintingTests.csproj", "{551D8E24-3373-48A8-ACE2-3E478D200D2A}" +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 + {551D8E24-3373-48A8-ACE2-3E478D200D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {551D8E24-3373-48A8-ACE2-3E478D200D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {551D8E24-3373-48A8-ACE2-3E478D200D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {551D8E24-3373-48A8-ACE2-3E478D200D2A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fb03cf882bcaf87aba661266cea196dd7e7d2eeb Mon Sep 17 00:00:00 2001 From: s1lenter Date: Fri, 28 Nov 2025 17:53:41 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ObjectPrinting/IPropertyPrintingConfig.cs | 4 +- ObjectPrinting/ITypePrintingConfig.cs | 4 +- ObjectPrinting/PrintingConfig.cs | 413 +++++++++---- ObjectPrinting/PropertyPrintingConfig.cs | 18 +- .../PropertyPrintingConfigExtensions.cs | 26 +- ObjectPrinting/Solved/ObjectExtensions.cs | 10 - ObjectPrinting/Solved/ObjectPrinter.cs | 10 - ObjectPrinting/Solved/PrintingConfig.cs | 62 -- .../Solved/PropertyPrintingConfig.cs | 32 - .../PropertyPrintingConfigExtensions.cs | 18 - .../Tests/ObjectPrinterAcceptanceTests.cs | 40 -- ObjectPrinting/Solved/Tests/Person.cs | 12 - .../Tests/ObjectPrinterAcceptanceTests.cs | 27 - ObjectPrinting/Tests/Person.cs | 12 - ...ernativePropertySerialization.verified.txt | 9 - ..._AlternativeTypeSerialization.verified.txt | 9 - ...intToString_CircularReference.verified.txt | 9 - ...PrintToString_ExcludeProperty.verified.txt | 8 - ...sts.PrintToString_ExcludeType.verified.txt | 8 - ....PrintToString_SerializeArray.verified.txt | 19 - ...tToString_SerializeDictionary.verified.txt | 23 - ...s.PrintToString_SerializeList.verified.txt | 19 - ...oString_SetCultureForProperty.verified.txt | 9 - ...intToString_SetCultureForType.verified.txt | 9 - ...interTests.PrintToString_Trim.verified.txt | 9 - ObjectPrinting/TypePrintingConfig.cs | 14 +- .../CollectionSerializeTests.cs | 133 ++++ ObjectPrintingTests/PrintingConfigTests.cs | 566 +++++++++++------- fluent-api.sln.DotSettings | 3 + 29 files changed, 832 insertions(+), 703 deletions(-) delete mode 100644 ObjectPrinting/Solved/ObjectExtensions.cs delete mode 100644 ObjectPrinting/Solved/ObjectPrinter.cs delete mode 100644 ObjectPrinting/Solved/PrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfig.cs delete mode 100644 ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs delete mode 100644 ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Solved/Tests/Person.cs delete mode 100644 ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs delete mode 100644 ObjectPrinting/Tests/Person.cs delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt delete mode 100644 ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt create mode 100644 ObjectPrintingTests/CollectionSerializeTests.cs diff --git a/ObjectPrinting/IPropertyPrintingConfig.cs b/ObjectPrinting/IPropertyPrintingConfig.cs index 08037fed1..d58756ec7 100644 --- a/ObjectPrinting/IPropertyPrintingConfig.cs +++ b/ObjectPrinting/IPropertyPrintingConfig.cs @@ -2,7 +2,7 @@ namespace ObjectPrinting; -public interface IPropertyPrintingConfig +public interface IPropertyPrintingConfig { - public PrintingConfig Using(Func func); + PrintingConfig Using(Func serializer); } \ No newline at end of file diff --git a/ObjectPrinting/ITypePrintingConfig.cs b/ObjectPrinting/ITypePrintingConfig.cs index b10f07881..d67f91632 100644 --- a/ObjectPrinting/ITypePrintingConfig.cs +++ b/ObjectPrinting/ITypePrintingConfig.cs @@ -2,7 +2,7 @@ namespace ObjectPrinting; -public interface ITypePrintingConfig +public interface ITypePrintingConfig { - public PrintingConfig Using(Func func); + PrintingConfig Using(Func serializer); } \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index 04adfd05c..a68679825 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -11,215 +11,388 @@ namespace ObjectPrinting; public class PrintingConfig { - private readonly HashSet typesToExclude = new(ReferenceEqualityComparer.Instance); - private readonly HashSet propertiesToExclude = []; - private readonly HashSet processedObjects = new(ReferenceEqualityComparer.Instance); - private readonly Dictionary cultureSerializers = []; - private readonly Dictionary> typeSerializers = []; - private readonly Dictionary> propertySerializers = []; - private int MaxNestingLevel { get; set; } = 5; + private readonly IReadOnlySet _typesToExclude; + private readonly IReadOnlySet _membersToExclude; + private readonly IReadOnlyDictionary _cultureSerializers; + private readonly IReadOnlyDictionary _typeSerializers; + private readonly IReadOnlyDictionary _memberSerializers; + private readonly int _maxNestingLevel; + + public PrintingConfig() + { + _typesToExclude = new HashSet(); + _membersToExclude = new HashSet(); + _cultureSerializers = new Dictionary(); + _typeSerializers = new Dictionary(); + _memberSerializers = new Dictionary(); + _maxNestingLevel = 5; + } + + private PrintingConfig( + IReadOnlySet typesToExclude, + IReadOnlySet membersToExclude, + IReadOnlyDictionary cultureSerializers, + IReadOnlyDictionary typeSerializers, + IReadOnlyDictionary memberSerializers, + int maxNestingLevel) + { + _typesToExclude = typesToExclude; + _membersToExclude = membersToExclude; + _cultureSerializers = cultureSerializers; + _typeSerializers = typeSerializers; + _memberSerializers = memberSerializers; + _maxNestingLevel = maxNestingLevel; + } public PrintingConfig SetMaxNestingLevel(int maxNestingLevel) { if (maxNestingLevel < 0) - { throw new ArgumentException("Max nesting level must be greater than or equal to 0."); - } - MaxNestingLevel = maxNestingLevel; - return this; + return new PrintingConfig( + _typesToExclude, + _membersToExclude, + _cultureSerializers, + _typeSerializers, + _memberSerializers, + maxNestingLevel); } public PrintingConfig Exclude() { - typesToExclude.Add(typeof(TPropType)); - return this; + var newTypesToExclude = new HashSet(_typesToExclude) { typeof(TPropType) }; + return new PrintingConfig( + newTypesToExclude, + _membersToExclude, + _cultureSerializers, + _typeSerializers, + _memberSerializers, + _maxNestingLevel); } public PrintingConfig Exclude(Expression> propertySelector) { - propertiesToExclude.Add(GetPropertyMemberInfo(propertySelector).Name); - return this; + var memberInfo = GetMemberInfo(propertySelector); + ValidateMemberInfo(memberInfo); + + var newMembersToExclude = new HashSet(_membersToExclude) { memberInfo }; + return new PrintingConfig( + _typesToExclude, + newMembersToExclude, + _cultureSerializers, + _typeSerializers, + _memberSerializers, + _maxNestingLevel); } - public IPropertyPrintingConfig PrintPropertySettings( - Expression> propertySelector) + public IPropertyPrintingConfig Printing(Expression> propertySelector) { - return new PropertyPrintingConfig(this, GetPropertyMemberInfo(propertySelector).Name); + var memberInfo = GetMemberInfo(propertySelector); + ValidateMemberInfo(memberInfo); + + return new PropertyPrintingConfig(this, memberInfo); } - public ITypePrintingConfig PrintSettings() + public ITypePrintingConfig Printing() { return new TypePrintingConfig(this); } + + public ITypePrintingConfig PrintSettings() + { + return Printing(); + } + public IPropertyPrintingConfig PrintPropertySettings( + Expression> propertySelector) + { + return Printing(propertySelector); + } + public PrintingConfig UseCulture(CultureInfo culture) where TPropType : IFormattable { - cultureSerializers[typeof(TPropType)] = culture; - return this; + return SetCulture(culture); } - internal void AddTypeSerializer(Func serializeFunc) + public PrintingConfig SetCulture(CultureInfo culture) where TPropType : IFormattable { - if (typeSerializers.TryGetValue(typeof(TPropType), out var serializers)) + var newCultureSerializers = new Dictionary(_cultureSerializers) { - serializers.Add(serializeFunc); - return; - } - typeSerializers[typeof(TPropType)] = [serializeFunc]; + [typeof(TPropType)] = culture + }; + + return new PrintingConfig( + _typesToExclude, + _membersToExclude, + newCultureSerializers, + _typeSerializers, + _memberSerializers, + _maxNestingLevel); } - internal void AddPropertySerializer(string propertyName, Func serializeFunc) + internal PrintingConfig WithTypeSerializer(Func serializeFunc) { - if (propertySerializers.TryGetValue(propertyName, out var serializers)) + var newTypeSerializers = new Dictionary(_typeSerializers) { - serializers.Add(serializeFunc); - return; - } - propertySerializers[propertyName] = [serializeFunc]; + [typeof(TPropType)] = serializeFunc + }; + + return new PrintingConfig( + _typesToExclude, + _membersToExclude, + _cultureSerializers, + newTypeSerializers, + _memberSerializers, + _maxNestingLevel); + } + + internal PrintingConfig WithMemberSerializer(MemberInfo memberInfo, Func serializeFunc) + { + var newMemberSerializers = new Dictionary(_memberSerializers) + { + [memberInfo] = serializeFunc + }; + + return new PrintingConfig( + _typesToExclude, + _membersToExclude, + _cultureSerializers, + _typeSerializers, + newMemberSerializers, + _maxNestingLevel); } public string PrintToString(TOwner obj) { - return PrintToString(obj, 1); + return PrintToString(obj, 1, new HashSet(ReferenceEqualityComparer.Instance)); } - private string PrintToString(object? obj, int nestingLevel) + private string PrintToString(object? obj, int nestingLevel, ISet processedObjects) { - if (obj is null) - { + if (obj == null) return "null"; - } + + var type = obj.GetType(); + + if ((type.IsPrimitive || type == typeof(string) || type == typeof(Guid) || type == typeof(DateTime)) + && _typesToExclude.Contains(type)) + return $""; if (processedObjects.Contains(obj)) + return $"Cyclic reference detected: {type.Name}"; + + if (_typeSerializers.TryGetValue(type, out var typeSerializer)) { - return "It is not possible to print an object with a circular reference."; + var result = typeSerializer.DynamicInvoke(obj) as string; + return result ?? "null"; } - var type = obj.GetType(); + if (type.IsPrimitive || type == typeof(string) || type == typeof(Guid) || type == typeof(DateTime)) + return type == typeof(string) ? obj.ToString() : FormatWithCulture(obj, type); - if (typeSerializers.TryGetValue(type, out var serializers)) - { - var result = serializers.Aggregate(obj, (current, serializer) => serializer.DynamicInvoke(current)!); + if (nestingLevel > _maxNestingLevel) + return $"... (max nesting level {_maxNestingLevel} reached)"; - return result.ToString()!; - } + processedObjects.Add(obj); - if (type.IsPrimitive || type == typeof(string) || type == typeof(Guid)) + try { - return type == typeof(string) ? $"\"{obj}\"" : obj.ToString()!; - } - - if (nestingLevel > MaxNestingLevel) - { - return $"The maximum recursion depth has been reached: {MaxNestingLevel}."; + if (obj is IEnumerable enumerable && type != typeof(string)) + return SerializeEnumerable(enumerable, nestingLevel, processedObjects); + + return SerializeObject(obj, nestingLevel, processedObjects, type); } - - processedObjects.Add(obj); - if (obj is IEnumerable enumerable && type != typeof(string)) + finally { - processedObjects.Clear(); - return SerializeEnumerable(enumerable, nestingLevel); + processedObjects.Remove(obj); } - + } + + private string FormatWithCulture(object obj, Type type) + { + if (_cultureSerializers.TryGetValue(type, out var culture) && obj is IFormattable formattable) + return formattable.ToString(null, culture); + + return obj.ToString() ?? "null"; + } + + private string SerializeObject(object obj, int nestingLevel, ISet processedObjects, Type type) + { var indentation = new string('\t', nestingLevel); - var stringBuilderResult = new StringBuilder(); - stringBuilderResult.AppendLine($"{type.Name}"); + var sb = new StringBuilder(); + sb.AppendLine(type.Name); + + var members = type.GetProperties().Cast() + .Concat(type.GetFields().Cast()); - foreach (var propertyInfo in type.GetProperties()) + foreach (var member in members) { - if (propertiesToExclude.Contains(propertyInfo.Name) || typesToExclude.Contains(propertyInfo.PropertyType)) - { + if (ShouldExcludeMember(member)) continue; - } - var propertyValue = propertyInfo.GetValue(obj); - var propertyType = propertyInfo.PropertyType; - var propertyName = propertyInfo.Name; + var value = GetMemberValue(member, obj); + var serializedValue = SerializeMember(member.Name, value, member.GetMemberType(), + nestingLevel, processedObjects, indentation, member); - stringBuilderResult.AppendLine(GetSerializeString(nestingLevel, propertyName, indentation, propertyValue, - propertyType)); + sb.AppendLine(serializedValue); } - return stringBuilderResult.ToString(); + return sb.ToString(); } - private string GetSerializeString( - int nestingLevel, - string propertyName, - string indentation, - object? propertyValue, - Type propertyType - ) + private bool ShouldExcludeMember(MemberInfo member) { - if (propertySerializers.TryGetValue(propertyName, out var serializers)) - { - var result = - serializers.Aggregate(propertyValue, (current, serializer) => serializer.DynamicInvoke(current)); + var memberType = member.GetMemberType(); + var shouldExclude = _typesToExclude.Contains(memberType) || + _membersToExclude.Contains(member); + + return shouldExclude; + } - return $"{indentation}{propertyName} = {result!}"; + private string SerializeMember(string memberName, object? value, Type memberType, + int nestingLevel, ISet processedObjects, string indentation, MemberInfo memberInfo) + { + if (_memberSerializers.TryGetValue(memberInfo, out var memberSerializer)) + { + var result = memberSerializer.DynamicInvoke(value); + return $"{indentation}{memberName} = {result}"; } - if (cultureSerializers.TryGetValue(propertyType, out var culture)) + if (value != null && _typeSerializers.TryGetValue(memberType, out var typeSerializer)) { - var propertyValueFormattable = propertyValue as IFormattable; - - return $"{indentation}{propertyName} = {propertyValueFormattable!.ToString(null, culture)}"; + var result = typeSerializer.DynamicInvoke(value); + return $"{indentation}{memberName} = {result}"; } + + if (value is IFormattable formattable && _cultureSerializers.TryGetValue(memberType, out var culture)) + return $"{indentation}{memberName} = {formattable.ToString(null, culture)}"; - return $"{indentation}{propertyName} = {PrintToString(propertyValue, nestingLevel + 1)}"; + return $"{indentation}{memberName} = {PrintToString(value, nestingLevel + 1, processedObjects)}"; } - private string SerializeEnumerable(IEnumerable enumerable, int nestingLevel) + private string SerializeEnumerable(IEnumerable enumerable, int nestingLevel, ISet processedObjects) +{ + if (enumerable is IDictionary dictionary) + return SerializeDictionary(dictionary, nestingLevel, processedObjects); + + return SerializeList(enumerable, nestingLevel, processedObjects); +} + + private string SerializeList(IEnumerable enumerable, int nestingLevel, ISet processedObjects) { - var objects = enumerable.Cast().ToList(); + var items = new List(); + int totalCount = 0; + const int maxItems = 100; + bool hasMoreItems = false; - if (objects.Count == 0) + foreach (var item in enumerable) { - return "{}"; + if (totalCount >= maxItems) + { + hasMoreItems = true; + break; + } + + items.Add(item); + totalCount++; } + if (items.Count == 0) + return "[]"; + var indentation = new string('\t', nestingLevel); - var serializeResult = new StringBuilder(); - serializeResult.AppendLine("{"); + var sb = new StringBuilder(); + sb.AppendLine("["); - if (enumerable is IDictionary dictionary) + for (int i = 0; i < items.Count; i++) { - foreach (var key in dictionary.Keys) - { - var value = dictionary[key]; - var keyConvert = PrintToString(key, nestingLevel); - var valueConvert = PrintToString(value, nestingLevel + 1); - - serializeResult.Append($"{indentation}{keyConvert}: {valueConvert}" + Environment.NewLine); - } + var serializedItem = PrintToString(items[i], nestingLevel + 1, processedObjects); + sb.Append($"{indentation}{serializedItem}"); + + if (i < items.Count - 1) + sb.AppendLine(","); + else + sb.AppendLine(); } - else + + if (hasMoreItems) + sb.AppendLine($"{indentation}... (showing first {maxItems} items)"); + + sb.Append($"{new string('\t', nestingLevel - 1)}]"); + return sb.ToString(); + } + + private string SerializeDictionary(IDictionary dictionary, int nestingLevel, ISet processedObjects) + { + var indentation = new string('\t', nestingLevel); + var sb = new StringBuilder(); + sb.AppendLine("["); + + int count = 0; + const int maxItems = 100; + bool hasMoreItems = false; + + foreach (DictionaryEntry entry in dictionary) { - for (var i = 0; i < objects.Count; i++) + if (count >= maxItems) { - var obj = objects[i]; - serializeResult.Append($"{indentation}{PrintToString(obj, nestingLevel + 1)}"); - if (i < objects.Count - 1) - { - serializeResult.Append($"{indentation},"); - serializeResult.Append(Environment.NewLine); - } + hasMoreItems = true; + break; } + + var serializedKey = PrintToString(entry.Key, nestingLevel + 1, processedObjects); + var serializedValue = PrintToString(entry.Value, nestingLevel + 1, processedObjects); + + sb.AppendLine($"{indentation}{serializedKey}: {serializedValue}"); + count++; } - serializeResult.AppendLine($"{indentation}}}"); + if (hasMoreItems) + sb.AppendLine($"{indentation}... (showing first {maxItems} items)"); - return serializeResult.ToString(); + sb.Append($"{new string('\t', nestingLevel - 1)}]"); + return count == 0 ? "[]" : sb.ToString(); } - private static MemberInfo GetPropertyMemberInfo(Expression> propertySelector) + private static MemberInfo GetMemberInfo(Expression> expression) { - return propertySelector.Body switch - { - MemberExpression memberExpression => memberExpression.Member, - UnaryExpression { Operand: MemberExpression operand } => operand.Member, - var _ => throw new ArgumentException("Invalid property selector expression") - }; + if (expression.Body is MemberExpression memberExpr) + return memberExpr.Member; + + if (expression.Body is UnaryExpression unaryExpr && unaryExpr.Operand is MemberExpression operandMemberExpr) + return operandMemberExpr.Member; + + throw new ArgumentException("Expression must be a property or field access"); + } + + private static void ValidateMemberInfo(MemberInfo memberInfo) + { + if (memberInfo.MemberType != MemberTypes.Property && memberInfo.MemberType != MemberTypes.Field) + throw new ArgumentException("Expression must reference a property or field"); + } + + private static object? GetMemberValue(MemberInfo member, object obj) + { + if (member is PropertyInfo property) + return property.GetValue(obj); + + if (member is FieldInfo field) + return field.GetValue(obj); + + return null; + } +} + +public static class MemberInfoExtensions +{ + public static Type GetMemberType(this MemberInfo member) + { + if (member is PropertyInfo property) + return property.PropertyType; + + if (member is FieldInfo field) + return field.FieldType; + + throw new ArgumentException("Member must be a property or field"); } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs index f26b05530..6b8a22e3c 100644 --- a/ObjectPrinting/PropertyPrintingConfig.cs +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -1,17 +1,21 @@ using System; +using System.Reflection; namespace ObjectPrinting; -public class PropertyPrintingConfig(PrintingConfig config, string propertyNameName) - : IPropertyPrintingConfig +public class PropertyPrintingConfig : IPropertyPrintingConfig { - private PrintingConfig Config { get; } = config; - private string PropertyName { get; } = propertyNameName; + private readonly PrintingConfig _parentConfig; + private readonly MemberInfo _memberInfo; - public PrintingConfig Using(Func func) + public PropertyPrintingConfig(PrintingConfig parentConfig, MemberInfo memberInfo) { - Config.AddPropertySerializer(PropertyName, func); + _parentConfig = parentConfig; + _memberInfo = memberInfo; + } - return Config; + public PrintingConfig Using(Func serializer) + { + return _parentConfig.WithMemberSerializer(_memberInfo, serializer); } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs index 0b5e8ef88..4a9bb21df 100644 --- a/ObjectPrinting/PropertyPrintingConfigExtensions.cs +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -1,11 +1,33 @@ +using System; + namespace ObjectPrinting; public static class PropertyPrintingConfigExtensions { public static PrintingConfig TrimmedTo( - this IPropertyPrintingConfig config, + this IPropertyPrintingConfig config, + int length) + { + return ImplementTrimmedTo(config, length); + } + + private static PrintingConfig ImplementTrimmedTo( + IPropertyPrintingConfig config, int length) { - return config.Using(str => str == null ? "null" : str[..length]); + if (length < 0) + throw new ArgumentException("Length must be non-negative", nameof(length)); + + return config.Using(str => + { + if (str == null) + return "null"; + + var stringValue = str.ToString(); + if (stringValue == null || stringValue.Length <= length) + return stringValue ?? "null"; + + return stringValue[..length]; + }); } } \ 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/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/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt deleted file mode 100644 index eca958436..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativePropertySerialization.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = NoName - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt deleted file mode 100644 index f0a7c193b..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_AlternativeTypeSerialization.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = date - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt deleted file mode 100644 index 5b7723d5c..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_CircularReference.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = Circular Reference diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt deleted file mode 100644 index 8226d3285..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeProperty.verified.txt +++ /dev/null @@ -1,8 +0,0 @@ -Person - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt deleted file mode 100644 index 94d0a1581..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_ExcludeType.verified.txt +++ /dev/null @@ -1,8 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt deleted file mode 100644 index 3feff8971..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeArray.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -Person[]: - Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null - Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bread - Height = 222 - Weight = 111 - Age = 22 - Birthday = 11/11/2001 00:00:00 - IsStudent = False - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt deleted file mode 100644 index 9797b02d8..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeDictionary.verified.txt +++ /dev/null @@ -1,23 +0,0 @@ -Dictionary`2: - KeyValuePair`2 - Key = Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null - Value = first - KeyValuePair`2 - Key = Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bread - Height = 222 - Weight = 111 - Age = 22 - Birthday = 11/11/2001 00:00:00 - IsStudent = False - Father = null - Value = second diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt deleted file mode 100644 index 498a0de81..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SerializeList.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -List`1: - Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null - Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bread - Height = 222 - Weight = 111 - Age = 22 - Birthday = 11/11/2001 00:00:00 - IsStudent = False - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt deleted file mode 100644 index 9091c50c5..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForProperty.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200,2 - Weight = 100.11 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt deleted file mode 100644 index 878e54b14..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_SetCultureForType.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bob - Height = 200,2 - Weight = 100,11 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt b/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt deleted file mode 100644 index 0a320f449..000000000 --- a/ObjectPrinting/Tests/Snapshots/ObjectPrinterTests.PrintToString_Trim.verified.txt +++ /dev/null @@ -1,9 +0,0 @@ -Person - Id = 00000000-0000-0000-0000-000000000000 - Name = Bo - Height = 200 - Weight = 100 - Age = 20 - Birthday = 10/10/2000 00:00:00 - IsStudent = True - Father = null diff --git a/ObjectPrinting/TypePrintingConfig.cs b/ObjectPrinting/TypePrintingConfig.cs index e9c141dcf..72745f3c6 100644 --- a/ObjectPrinting/TypePrintingConfig.cs +++ b/ObjectPrinting/TypePrintingConfig.cs @@ -2,15 +2,17 @@ namespace ObjectPrinting; -public class TypePrintingConfig(PrintingConfig config) - : ITypePrintingConfig +public class TypePrintingConfig : ITypePrintingConfig { - private PrintingConfig Config { get; } = config; + private readonly PrintingConfig _parentConfig; - public PrintingConfig Using(Func func) + public TypePrintingConfig(PrintingConfig parentConfig) { - Config.AddTypeSerializer(func); + _parentConfig = parentConfig; + } - return Config; + public PrintingConfig Using(Func serializer) + { + return _parentConfig.WithTypeSerializer(serializer); } } \ No newline at end of file diff --git a/ObjectPrintingTests/CollectionSerializeTests.cs b/ObjectPrintingTests/CollectionSerializeTests.cs new file mode 100644 index 000000000..af34e1fc4 --- /dev/null +++ b/ObjectPrintingTests/CollectionSerializeTests.cs @@ -0,0 +1,133 @@ +using FluentAssertions; +using ObjectPrinting; +using ObjectPrinting.Test; + +namespace ObjectPrintingTests; + +[TestFixture] +public class CollectionSerializeTests +{ + [Test] + public void PrintToString_ShouldSerializeListCorrectly() + { + var list = new List { 1, 2, 3, 4, 5 }; + + var result = ObjectPrinter.For>().PrintToString(list); + + var expected = "[\r\n\t1,\r\n\t2,\r\n\t3,\r\n\t4,\r\n\t5\r\n]"; + result.Should().Be(expected); + } + + [Test] + public void PrintToString_ShouldSerializeEmptyList() + { + var list = new List(); + + var result = ObjectPrinter.For>().PrintToString(list); + + result.Should().Be("[]"); + } + + [Test] + public void PrintToString_ShouldSerializeArray() + { + var array = new[] { "a", "b", "c" }; + + var result = ObjectPrinter.For().PrintToString(array); + + var expected = "[\r\n\ta,\r\n\tb,\r\n\tc\r\n]"; + result.Should().Be(expected); + } + + [Test] + public void PrintToString_ShouldSerializeDictionary() + { + var dict = new Dictionary + { + { "one", 1 }, + { "two", 2 } + }; + + var result = ObjectPrinter.For>().PrintToString(dict); + + result.Should().Contain("one").And.Contain("1") + .And.Contain("two").And.Contain("2") + .And.Contain("["); + } + + [Test] + public void PrintToString_ShouldSerializeNestedCollections() + { + var matrix = new List> + { + new() { 1, 2 }, + new() { 3, 4 } + }; + + var result = ObjectPrinter.For>>().PrintToString(matrix); + + result.Should().Contain("1").And.Contain("2") + .And.Contain("3").And.Contain("4") + .And.Contain("["); + } + + [Test] + public void PrintToString_ShouldSerializeCollectionWithObjects() + { + var people = new List + { + new() { Name = "John", Age = 25 }, + new() { Name = "Jane", Age = 30 } + }; + + var result = ObjectPrinter.For>().PrintToString(people); + + result.Should().Contain("John").And.Contain("25") + .And.Contain("Jane").And.Contain("30") + .And.Contain("Person") + .And.Contain("["); + } + + [Test] + public void PrintToString_ShouldHandleNullInCollections() + { + var listWithNulls = new List { "a", null, "c" }; + + var result = ObjectPrinter.For>().PrintToString(listWithNulls); + + var expected = "[\r\n\ta,\r\n\tnull,\r\n\tc\r\n]"; + result.Should().Be(expected); + } + + [Test] + public void ExcludeType_ShouldWorkWithCollections() + { + var people = new List + { + new() { Name = "John", Age = 25 }, + new() { Name = "Jane", Age = 30 } + }; + + var result = ObjectPrinter.For>() + .Exclude() + .PrintToString(people); + + result.Should().NotContain("Name =") + .And.Contain("Age = 25") + .And.Contain("Age = 30"); + } + + [Test] + public void PrintSettings_ShouldApplyToCollectionElements() + { + var numbers = new List { 10, 20, 30 }; + + var result = ObjectPrinter.For>() + .PrintSettings() + .Using(i => $"#{i}#") + .PrintToString(numbers); + + var expected = "[\r\n\t#10#,\r\n\t#20#,\r\n\t#30#\r\n]"; + result.Should().Be(expected); + } +} \ No newline at end of file diff --git a/ObjectPrintingTests/PrintingConfigTests.cs b/ObjectPrintingTests/PrintingConfigTests.cs index df74958a8..7d211171e 100644 --- a/ObjectPrintingTests/PrintingConfigTests.cs +++ b/ObjectPrintingTests/PrintingConfigTests.cs @@ -1,10 +1,11 @@ using System.Globalization; -using System.Text; using FluentAssertions; +using ObjectPrinting.Test; -namespace ObjectPrinting.Test; +namespace ObjectPrinting.Tests; -public class TestsObjectPrinting +[TestFixture] +public class ObjectPrintingComprehensiveTests { private Person firstPerson; private Person secondPerson; @@ -29,346 +30,491 @@ public void Setup() }; secondPerson = new(); - family = new() { Mom = firstPerson, Dad = secondPerson, Children = [firstPerson, secondPerson] }; } [Test] - public void Exclude_ExcludeType() + public void PrintToString_ShouldSerializePersonWithAllProperties() + { + var result = ObjectPrinter.For().PrintToString(firstPerson); + + result.Should().Contain("Name = Ben") + .And.Contain("Surname = Big") + .And.Contain("Age = 20") + .And.Contain("Height = 170,1") + .And.Contain("BestFriend = Person"); + } + + [Test] + public void PrintToString_ShouldSerializeNestedBestFriend() + { + var result = ObjectPrinter.For().PrintToString(firstPerson); + + result.Should().Contain("BestFriend = Person") + .And.Contain("Name = Bob") + .And.Contain("Surname = Boby") + .And.Contain("Age = 80") + .And.Contain("Height = 40"); + } + + [Test] + public void PrintToString_ShouldSerializeFriendsList() + { + var result = ObjectPrinter.For().PrintToString(firstPerson); + + result.Should().Contain("Friends = [") + .And.Contain("Name = Alice") + .And.Contain("Name = Max") + .And.Contain("Surname = Sev") + .And.Contain("Surname = Albor"); + } + + [Test] + public void PrintToString_ShouldSerializeBodyPartsDictionary() + { + var result = ObjectPrinter.For().PrintToString(firstPerson); + + result.Should().Contain("BodyParts = [") + .And.Contain("Hand") + .And.Contain("Foot") + .And.Contain("Head") + .And.Contain("Tail") + .And.Contain("2") + .And.Contain("1") + .And.Contain("0"); + } + + [Test] + public void ExcludeTypeString_ShouldRemoveNameAndSurname() { - const string unexpectedName = nameof(Person.Name); - const string unexpectedSurname = nameof(Person.Surname); var result = ObjectPrinter.For() .Exclude() .PrintToString(firstPerson); - result.Should().NotContain(unexpectedName).And.NotContain(unexpectedSurname); + result.Should().NotContain("Name =") + .And.NotContain("Surname =") + .And.Contain("Age = 20") + .And.Contain("Height = 170,1"); } [Test] - public void Exclude_ExcludeProperty() + public void ExcludePropertyAge_ShouldRemoveOnlyAge() { - const string unexpectedAge = nameof(Person.Age); var result = ObjectPrinter.For() .Exclude(p => p.Age) .PrintToString(firstPerson); - result.Should().NotContain(unexpectedAge); + result.Should().NotContain("Age =") + .And.Contain("Name = Ben") + .And.Contain("Surname = Big") + .And.Contain("Height = 170,1"); } [Test] - public void Exclude_AddSerializationToType_ThenExcludeType() + public void PrintSettingsForInt_ShouldFormatAgeInHex() { - var unexpectedName = $"{firstPerson.Name} is beautiful"; - var unexpectedSurname = $"{firstPerson.Surname} is beautiful"; var result = ObjectPrinter.For() - .PrintSettings().Using(p => $"{p} is beautiful") - .Exclude() + .PrintSettings() + .Using(i => i.ToString("X")) .PrintToString(firstPerson); - result.Should().NotContain(unexpectedName).And.NotContain(unexpectedSurname); + result.Should().Contain("Age = 14") + .And.Contain("BodyParts"); } [Test] - public void Exclude_ExcludeAllProperties() + public void PrintPropertySettingsForName_ShouldApplyCustomFormatting() { - const string expected = "Person\r\n"; var result = ObjectPrinter.For() - .Exclude(p => p.Id) - .Exclude(p => p.Name) - .Exclude(p => p.Surname) - .Exclude(p => p.Height) - .Exclude(p => p.Age) - .Exclude(p => p.BestFriend) - .Exclude(p => p.Friends) - .Exclude(p => p.BodyParts) + .PrintPropertySettings(p => p.Name) + .Using(name => $"Mr. {name}") .PrintToString(firstPerson); - result.Should().Be(expected); + result.Should().Contain("Name = Mr. Ben") + .And.Contain("Surname = Big"); } [Test] - public void Exclude_ExcludeAllTypes() + public void TrimmedTo_ShouldShortenName() { - const string expected = "Person\r\n"; var result = ObjectPrinter.For() - .Exclude() - .Exclude() - .Exclude() - .Exclude() - .Exclude() - .Exclude() - .Exclude>() + .PrintPropertySettings(p => p.Name) + .TrimmedTo(2) .PrintToString(firstPerson); - result.Should().Be(expected); + result.Should().Contain("Name = Be") + .And.Contain("Surname = Big"); } [Test] - public void Using_SerializationForString() + public void UseCultureForDouble_ShouldFormatHeightWithCulture() { - const string expected = " is beautiful"; var result = ObjectPrinter.For() - .PrintSettings() - .Using(p => $"{p} is beautiful") + .UseCulture(new CultureInfo("en-US")) .PrintToString(firstPerson); - result.Should().Contain(expected); + result.Should().Contain("Height = 170.1"); } [Test] - public void Using_SerializationForInt() + public void PrintToString_ShouldHandleCircularReferencesInFamily() + { + var result = ObjectPrinter.For().PrintToString(family); + + result.Should().NotContain("StackOverflow") + .And.NotContain("Infinite loop"); + } + + [Test] + public void SetMaxNestingLevel_ShouldLimitComplexObjectDepth() { - const string exceptedAge = $"{nameof(firstPerson.Age)} = 10"; var result = ObjectPrinter.For() - .PrintSettings() - .Using(_ => "10") + .SetMaxNestingLevel(2) .PrintToString(firstPerson); - result.Should().Contain(exceptedAge); + result.Should().Contain("max nesting level"); } [Test] - public void Using_SerializationForName() + public void ExcludeBestFriend_ShouldRemoveNestedObject() { - var expected = firstPerson.Name?.ToUpper(); - var unexpected = firstPerson.Name; var result = ObjectPrinter.For() - .PrintPropertySettings(p => p.Name) - .Using(p => p!.ToUpper()) + .Exclude(p => p.BestFriend) .PrintToString(firstPerson); - result.Should().Contain(expected).And.NotContain(unexpected); + result.Should().NotContain("BestFriend = Person") + .And.Contain("Name = Ben") + .And.Contain("Friends = ["); } [Test] - public void Using_MultiplePropertySerializations() + public void ExcludeFriends_ShouldRemoveListButKeepOtherProperties() { - var exceptedAge = $"{nameof(firstPerson.Name)} = 21{firstPerson.Name}12"; var result = ObjectPrinter.For() - .PrintPropertySettings(p => p.Name) - .Using(name => $"1{name}1") - .PrintPropertySettings(p => p.Name) - .Using(name => $"2{name}2") + .Exclude(p => p.Friends) .PrintToString(firstPerson); - result.Should().Contain(exceptedAge); + result.Should().NotContain("Friends = [") + .And.Contain("Name = Ben") + .And.Contain("BestFriend = Person") + .And.Contain("BodyParts = ["); } [Test] - public void Using_MultipleTypeSerializationsForString() + public void MultipleExclusions_ShouldWorkTogether() { - var exceptedName = $"{nameof(firstPerson.Name)} = 21{firstPerson.Name}12"; var result = ObjectPrinter.For() - .PrintSettings() - .Using(str => $"1{str}1") - .PrintSettings() - .Using(str => $"2{str}2") + .Exclude(p => p.Age) + .Exclude(p => p.Height) + .Exclude(p => p.BestFriend) .PrintToString(firstPerson); - result.Should().Contain(exceptedName); + result.Should().NotContain("Age =") + .And.NotContain("Height =") + .And.NotContain("BestFriend =") + .And.Contain("Name = Ben") + .And.Contain("Friends = [") + .And.Contain("BodyParts = ["); } [Test] - public void Using_TrimmedToPropertiesAfterSerializationForName([Values(0, 1, 2)] int length) + public void TypeAndPropertyExclusionCombination_ShouldWork() { - var exceptedName = $"{nameof(firstPerson.Name)} = " + $"1{firstPerson.Name!}"[..length]; var result = ObjectPrinter.For() - .PrintPropertySettings(p => p.Name) - .Using(name => $"1{name}1") - .PrintPropertySettings(p => p.Name) - .TrimmedTo(length) + .Exclude() + .Exclude(p => p.Age) .PrintToString(firstPerson); - result.Should().Contain(exceptedName); + result.Should().NotContain("Name =") + .And.NotContain("Surname =") + .And.NotContain("Age =") + .And.Contain("Height = 170,1") + .And.Contain("BestFriend = Person"); } [Test] - public void UseCulture_ChangeCultureForDouble() + public void PrintSettingsForString_ShouldAffectAllStringProperties() { - var expected = firstPerson.Height.ToString(new CultureInfo("ru-RU")); - var unexpected = firstPerson.Height.ToString(new CultureInfo("en-US")); var result = ObjectPrinter.For() - .UseCulture(new("ru-RU")) + .PrintSettings() + .Using(str => str?.ToUpper() ?? "NULL") .PrintToString(firstPerson); - result.Should().Contain(expected).And.NotContain(unexpected); + result.Should().Contain("Name = BEN") + .And.Contain("Surname = BIG") + .And.Contain("BestFriend = Person") + .And.Contain("Name = BOB"); } [Test] - public void TrimmedTo_TrimmingName([Values(0, 1, 2)] int length) + public void ComplexConfiguration_ShouldApplyAllSettings() { - var expected = $"{nameof(firstPerson.Name)} = {firstPerson.Name?[..length]}"; - var unexpected = firstPerson.Name; var result = ObjectPrinter.For() - .PrintPropertySettings(p => p.Name) - .TrimmedTo(length) + .Exclude() + .PrintSettings().Using(i => (i * 10).ToString()) + .PrintPropertySettings(p => p.Name).TrimmedTo(1) + .UseCulture(new CultureInfo("en-US")) + .Exclude(p => p.Surname) .PrintToString(firstPerson); - result.Should().Contain(expected).And.NotContain(unexpected); + result.Should().NotContain("Id") + .And.NotContain("Surname =") + .And.Contain("Age = 200") + .And.Contain("Name = B") + .And.Contain("Height = 170.1") + .And.Contain("BestFriend = Person"); + } + + [Test] + public void PrintToString_ExtensionMethod_ShouldWorkWithFirstPerson() + { + var result = firstPerson.PrintToString(); + + result.Should().Contain("Name = Ben") + .And.Contain("Age = 20") + .And.Contain("BestFriend = Person"); + } + + [Test] + public void PrintToString_WithConfigExtension_ShouldWork() + { + var result = firstPerson.PrintToString(config => + config.Exclude(p => p.Age) + .PrintPropertySettings(p => p.Name).TrimmedTo(1)); + + result.Should().NotContain("Age =") + .And.Contain("Name = B") + .And.Contain("Surname = Big"); + } + + [Test] + public void PrintToString_ShouldHandleEmptySecondPerson() + { + var result = ObjectPrinter.For().PrintToString(secondPerson); + + result.Should().Contain("Person") + .And.Contain("Name = null") + .And.Contain("Age = 0") + .And.Contain("Height = 0") + .And.Contain("BestFriend = null") + .And.Contain("Friends = []") + .And.Contain("BodyParts = []"); + } + + [Test] + public void PrintToString_ShouldSerializeFamilyStructure() + { + var result = ObjectPrinter.For().PrintToString(family); + + result.Should().Contain("Mom = Person") + .And.Contain("Dad = Person") + .And.Contain("Children = [") + .And.Contain("Name = Ben") + .And.Contain("Name = null"); } - [TestCase(0)] - [TestCase(1)] - [TestCase(2)] - public void SetMaxNestingLevel_WithDifferentNestingLevels(int levelNesting) + [Test] + public void PropertySerialization_ShouldOverrideTypeSerializationForSpecificProperty() { - firstPerson.BestFriend = secondPerson; - var expected = $"The maximum recursion depth has been reached: {levelNesting}."; var result = ObjectPrinter.For() - .SetMaxNestingLevel(levelNesting) + .PrintSettings().Using(str => $"TYPE_{str}") + .PrintPropertySettings(p => p.Name).Using(str => $"PROP_{str}") .PrintToString(firstPerson); - result.Should().Contain(expected); + result.Should().Contain("Name = PROP_Ben") + .And.Contain("Surname = TYPE_Big"); } - [TestCase(-1)] - [TestCase(-2)] - [TestCase(-3)] - [TestCase(-4)] - [TestCase(-5)] - public void SetMaxNestingLevel_WithNegativeNestingLevel(int levelNesting) + [Test] + public void PrintToString_ShouldNotHaveStackOverflowWithComplexStructure() { - FluentActions.Invoking(() => ObjectPrinter.For() - .SetMaxNestingLevel(levelNesting)) - .Should().Throw(); + var action = () => ObjectPrinter.For().PrintToString(firstPerson); + + action.Should().NotThrow(); } [Test] - public void PrintToString_ProcessingCyclicLinksBetweenObjects() + public void MultipleTypeSerializations_LastOneShouldWin() { - firstPerson.BestFriend = secondPerson; - secondPerson.BestFriend = firstPerson; + var result = ObjectPrinter.For() + .PrintSettings().Using(str => "first") + .PrintSettings().Using(str => "second") + .PrintToString(firstPerson); + result.Should().Contain("Name = second") + .And.Contain("Surname = second") + .And.NotContain("first"); + } + + [Test] + public void MultiplePropertySerializations_LastOneShouldWin() + { var result = ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .Using(name => "first") + .PrintPropertySettings(p => p.Name) + .Using(name => "second") .PrintToString(firstPerson); - result.Should().Contain("It is not possible to print an object with a circular reference."); + + result.Should().Contain("Name = second") + .And.NotContain("first"); } [Test] - public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_Array() + public void Exclusion_ShouldHavePriorityOverSerialization() { - var result = ObjectPrinter.For() - .PrintToString([firstPerson, firstPerson]); + var result = ObjectPrinter.For() + .PrintSettings().Using(str => "modified") + .Exclude() + .PrintToString(firstPerson); - result.Should().NotContain("It is not possible to print an object with a circular reference."); + result.Should().NotContain("Name =") + .And.NotContain("Surname =") + .And.NotContain("modified"); } [Test] - public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_List() + public void PrintToString_ShouldHandleDictionaryValuesCorrectly() { - var result = ObjectPrinter.For>() - .PrintToString([firstPerson, firstPerson]); + var result = ObjectPrinter.For().PrintToString(firstPerson); - result.Should().NotContain("It is not possible to print an object with a circular reference."); + result.Should().Contain("Hand").And.Contain("2") + .And.Contain("Foot").And.Contain("2") + .And.Contain("Head").And.Contain("1") + .And.Contain("Tail").And.Contain("0"); } [Test] - public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_Dictionary() + public void SetMaxNestingLevel_WithValidValue_ShouldNotThrow() { - var result = ObjectPrinter.For>() - .PrintToString(new() { { firstPerson, firstPerson } }); + var action = () => ObjectPrinter.For() + .SetMaxNestingLevel(5) + .PrintToString(firstPerson); - result.Should().NotContain("It is not possible to print an object with a circular reference."); + action.Should().NotThrow(); } [Test] - public void PrintToString_WhenLinkIsSame_ButThereAreNoCircularReferences_InsideObject() + public void TrimmedTo_WithValidLength_ShouldNotThrow() + { + var action = () => ObjectPrinter.For() + .PrintPropertySettings(p => p.Name) + .TrimmedTo(3) + .PrintToString(firstPerson); + + action.Should().NotThrow(); + } + + [Test] + public void PrintToString_ShouldMaintainObjectStructureIntegrity() + { + var result = ObjectPrinter.For().PrintToString(firstPerson); + + result.Should().Contain("Person") + .And.Contain("BestFriend = Person") + .And.Contain("Friends = [") + .And.Contain("BodyParts = [") + .And.Contain("]"); + } + + [Test] + public void PropertyExclusion_ShouldBeClassSpecific() { - family = family with { Mom = firstPerson, Dad = firstPerson }; var result = ObjectPrinter.For() + .Exclude(f => f.Mom.Name) .PrintToString(family); - result.Should().NotContain("It is not possible to print an object with a circular reference."); - } - - [Test] - public void PrintToString_CorrectStructure() - { - var expectedResult = new StringBuilder(); - expectedResult.Append("Person\r\n\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t" + - "Name = \"Ben\"\r\n\t" + - "Surname = \"Big\"\r\n\t" + - "Height = 170,1\r\n\t" + - "Age = 20\r\n\t" + - "BestFriend = Person\r\n\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t" + - "Name = \"Bob\"\r\n\t\t" + - "Surname = \"Boby\"\r\n\t\t" + - "Height = 40\r\n\t\t" + - "Age = 80\r\n\t\t" + - "BestFriend = null\r\n\t\t" + - "Friends = {}\r\n\t\t" + - "BodyParts = {}\r\n\r\n\t" + - "Friends = {\r\n\t\t" + - "Person\r\n\t\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + - "Name = \"Alice\"\r\n\t\t\t" + - "Surname = \"Sev\"\r\n\t\t\t" + - "Height = 50\r\n\t\t\t" + - "Age = 30\r\n\t\t\t" + - "BestFriend = null\r\n\t\t\t" + - "Friends = {}\r\n\t\t\t" + - "BodyParts = {}\r\n\t\t,\r\n\t\t" + - "Person\r\n\t\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + - "Name = \"Max\"\r\n\t\t\t" + - "Surname = \"Albor\"\r\n\t\t\t" + - "Height = 10\r\n\t\t\t" + - "Age = 9\r\n\t\t\t" + - "BestFriend = null\r\n\t\t\t" + - "Friends = {}\r\n\t\t\t" + - "BodyParts = {}\r\n\t\t" + - "}\r\n\r\n\t" + - "BodyParts = {\r\n\t\t\"Hand\": 2\r\n\t\t\"Foot\": 2\r\n\t\t\"Head\": 1\r\n\t\t\"Tail\": 0\r\n\t\t}\r\n\r\n"); - - var result = expectedResult.ToString(); - var v2 = ObjectPrinter.For() - .PrintToString(firstPerson); + var lines = result.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + var momLineIndex = Array.FindIndex(lines, line => line.Trim() == "Mom = Person"); + momLineIndex.Should().BeGreaterThan(-1, "Mom should be present"); - result.Should().Contain(v2); - } - - [Test] - public void PrintToString_WithExcludedProperiesStructure() - { - var expectedResult = new StringBuilder(); - expectedResult.Append("Person\r\n\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t" + - "Surname = \"Big\"\r\n\t" + - "Height = 170,1\r\n\t" + - "BestFriend = Person\r\n\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t" + - "Surname = \"Boby\"\r\n\t\t" + - "Height = 40\r\n\t\t" + - "BestFriend = null\r\n\t\t" + - "Friends = {}\r\n\t\t" + - "BodyParts = {}\r\n\r\n\t" + - "Friends = {\r\n\t\t" + - "Person\r\n\t\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + - "Surname = \"Sev\"\r\n\t\t\t" + - "Height = 50\r\n\t\t\t" + - "BestFriend = null\r\n\t\t\t" + - "Friends = {}\r\n\t\t\t" + - "BodyParts = {}\r\n\t\t,\r\n\t\t" + - "Person\r\n\t\t\t" + - "Id = 00000000-0000-0000-0000-000000000000\r\n\t\t\t" + - "Surname = \"Albor\"\r\n\t\t\t" + - "Height = 10\r\n\t\t\t" + - "BestFriend = null\r\n\t\t\t" + - "Friends = {}\r\n\t\t\t" + - "BodyParts = {}\r\n\t\t" + - "}\r\n\r\n\t" + - "BodyParts = {\r\n\t\t\"Hand\": 2\r\n\t\t\"Foot\": 2\r\n\t\t\"Head\": 1\r\n\t\t\"Tail\": 0\r\n\t\t}\r\n\r\n"); - - var result = expectedResult.ToString(); - var v2 = ObjectPrinter.For() - .Exclude(p => p.Name) - .Exclude(p => p.Age) - .PrintToString(firstPerson); + var linesAfterMom = lines.Skip(momLineIndex + 1).Take(10).ToArray(); + var hasMomName = linesAfterMom.Any(line => line.Contains("Name = Ben")); + hasMomName.Should().BeFalse("Mom.Name should be excluded"); + + result.Should().Contain("Dad = Person"); + } + + [Test] + public void PropertySerialization_ShouldBeClassSpecific() + { + var result = ObjectPrinter.For() + .PrintPropertySettings(f => f.Mom.Name) + .Using(name => $"MOM_{name}") + .PrintToString(family); + + result.Should().Contain("MOM_Ben"); + + var hasRegularName = result.Contains("Name = null") || result.Contains("Surname = Big"); + hasRegularName.Should().BeTrue("There should be regular non-modified properties"); + } + + [Test] + public void LargeCollection_ShouldBeLimited() + { + var largeList = Enumerable.Range(1, 150).ToList(); + + var result = ObjectPrinter.For>().PrintToString(largeList); + + result.Should().Contain("100") + .And.Contain("... (showing first 100 items)") + .And.NotContain("101"); + } + + [Test] + public void EmptyCollection_ShouldSerializeAsEmpty() + { + var emptyList = new List(); + + var result = ObjectPrinter.For>().PrintToString(emptyList); + + result.Should().Be("[]"); + } + + [Test] + public void CollectionWithFewItems_ShouldShowAll() + { + var smallList = new List { 1, 2, 3 }; + + var result = ObjectPrinter.For>().PrintToString(smallList); + + result.Should().Contain("1") + .And.Contain("2") + .And.Contain("3") + .And.NotContain("..."); + } + + [Test] + public void Dictionary_ShouldBeLimitedWhenLarge() + { + var largeDict = new Dictionary(); + for (int i = 0; i < 150; i++) + { + largeDict[$"key{i}"] = i; + } + + var result = ObjectPrinter.For>().PrintToString(largeDict); + + result.Should().Contain("key0") + .And.Contain("... (showing first 100 items)") + .And.NotContain("key100"); + } + + [Test] + public void NestedCollections_ShouldRespectLimits() + { + var nestedList = new List> + { + Enumerable.Range(1, 50).ToList(), + Enumerable.Range(51, 60).ToList() + }; + + var result = ObjectPrinter.For>>().PrintToString(nestedList); - result.Should().Contain(v2); + result.Should().Contain("1") + .And.Contain("50") + .And.Contain("51") + .And.Contain("110"); } } \ No newline at end of file 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