From 69e50d6eb60dc34a61247236670c12222b753681 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Wed, 19 Nov 2025 23:05:43 +0500 Subject: [PATCH] All project, without reafactor and perfomance tests --- ObjectPrinting/CollectionSerializer.cs | 120 +++++++++++++++ ObjectPrinting/CollectionsConfigurator.cs | 41 ++++++ .../CollectionsConfiguratorExtensions.cs | 33 +++++ ObjectPrinting/ObjectPrinter.cs | 1 + ObjectPrinting/ObjectPrinterExtension.cs | 18 +++ ObjectPrinting/ObjectPrinting.csproj | 1 + ObjectPrinting/ObjectSerializer.cs | 117 +++++++++++++++ ObjectPrinting/PrintingConfig.cs | 66 +++++---- ObjectPrinting/PrintingConfigData.cs | 17 +++ ObjectPrinting/PropertyConfigurator.cs | 30 ++++ ObjectPrinting/PropertyExtension.cs | 27 ++++ .../Tests/BasicSerializationTests.cs | 71 +++++++++ .../Tests/CircularReferenceTests.cs | 97 ++++++++++++ .../Tests/CollectionConfigurationTests.cs | 139 ++++++++++++++++++ .../Tests/CollectionSerializationTests.cs | 89 +++++++++++ .../Tests/CombinedScenariosTests.cs | 117 +++++++++++++++ .../Tests/CultureSerializationTests.cs | 68 +++++++++ .../Tests/DictionarySerializationTests.cs | 78 ++++++++++ ObjectPrinting/Tests/EdgeCasesTests.cs | 57 +++++++ .../Tests/ObjectPrinterAcceptanceTests.cs | 42 ++++-- ObjectPrinting/Tests/Person.cs | 18 ++- ObjectPrinting/Tests/StringTrimmingTests.cs | 52 +++++++ ObjectPrinting/TypeConfigurator.cs | 21 +++ ObjectPrinting/TypeConfiguratorExtensions.cs | 26 ++++ fluent-api.sln.DotSettings | 3 + 25 files changed, 1310 insertions(+), 39 deletions(-) create mode 100644 ObjectPrinting/CollectionSerializer.cs create mode 100644 ObjectPrinting/CollectionsConfigurator.cs create mode 100644 ObjectPrinting/CollectionsConfiguratorExtensions.cs create mode 100644 ObjectPrinting/ObjectPrinterExtension.cs create mode 100644 ObjectPrinting/ObjectSerializer.cs create mode 100644 ObjectPrinting/PrintingConfigData.cs create mode 100644 ObjectPrinting/PropertyConfigurator.cs create mode 100644 ObjectPrinting/PropertyExtension.cs create mode 100644 ObjectPrinting/Tests/BasicSerializationTests.cs create mode 100644 ObjectPrinting/Tests/CircularReferenceTests.cs create mode 100644 ObjectPrinting/Tests/CollectionConfigurationTests.cs create mode 100644 ObjectPrinting/Tests/CollectionSerializationTests.cs create mode 100644 ObjectPrinting/Tests/CombinedScenariosTests.cs create mode 100644 ObjectPrinting/Tests/CultureSerializationTests.cs create mode 100644 ObjectPrinting/Tests/DictionarySerializationTests.cs create mode 100644 ObjectPrinting/Tests/EdgeCasesTests.cs create mode 100644 ObjectPrinting/Tests/StringTrimmingTests.cs create mode 100644 ObjectPrinting/TypeConfigurator.cs create mode 100644 ObjectPrinting/TypeConfiguratorExtensions.cs diff --git a/ObjectPrinting/CollectionSerializer.cs b/ObjectPrinting/CollectionSerializer.cs new file mode 100644 index 000000000..dba1ce7a1 --- /dev/null +++ b/ObjectPrinting/CollectionSerializer.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ObjectPrinting +{ + internal static class CollectionSerializer + { + public static bool IsCollection(Type type) + { + if (type.IsArray) + return true; + + return typeof(IEnumerable).IsAssignableFrom(type) && + type != typeof(string); + } + + public static bool IsDictionary(Type type) + { + if (typeof(IDictionary).IsAssignableFrom(type)) + return true; + + return type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + } + + public static string SerializeCollection(object collection, int nestingLevel, HashSet visitedObjects, PrintingConfigData config) + { + var sb = new StringBuilder(); + var identation = new string('\t', nestingLevel + 1); + var collectionType = collection.GetType(); + + sb.AppendLine("["); + + var enumerable = (IEnumerable)collection; + var items = enumerable.Cast().ToList(); + var maxItems = config.CollectionMaxItems.GetValueOrDefault(collectionType, int.MaxValue); + + foreach (var item in items.Take(maxItems)) + { + string serializedItem; + if (config.CollectionSerializers.TryGetValue(collectionType, out var serializer)) + { + serializedItem = serializer(item); + } + else + { + var objectSerializer = new ObjectSerializer(config); + serializedItem = objectSerializer.PrintToString(item, nestingLevel + 1, visitedObjects).TrimEnd(); + } + + sb.AppendLine(identation + serializedItem + ","); + } + + if (items.Count > maxItems) + { + var remaining = items.Count - maxItems; + sb.AppendLine(identation + $"... and {remaining} more items"); + } + else if (items.Count == 0) + { + sb.AppendLine(identation + "empty"); + } + + sb.AppendLine("]"); + return sb.ToString(); + } + + public static string SerializeDictionary(object dictionary, int nestingLevel, HashSet visitedObjects, PrintingConfigData config) + { + var sb = new StringBuilder(); + var identation = new string('\t', nestingLevel + 1); + var dictType = dictionary.GetType(); + + sb.AppendLine("["); + + try + { + var dict = (IDictionary)dictionary; + var count = 0; + var maxItems = config.CollectionMaxItems.GetValueOrDefault(dictType, int.MaxValue); + var objectSerializer = new ObjectSerializer(config); + + foreach (DictionaryEntry entry in dict) + { + if (count >= maxItems) + { + var remaining = dict.Count - maxItems; + sb.AppendLine(identation + $"... and {remaining} more items"); + break; + } + + var serializedKey = objectSerializer.PrintToString(entry.Key, nestingLevel + 1, visitedObjects).Trim(); + var serializedValue = objectSerializer.PrintToString(entry.Value, nestingLevel + 1, visitedObjects).Trim(); + + serializedKey = serializedKey.Replace(Environment.NewLine, " ").Trim(); + serializedValue = serializedValue.Replace(Environment.NewLine, " ").Trim(); + + sb.AppendLine(identation + $"{serializedKey}: {serializedValue},"); + count++; + } + + if (count == 0) + { + sb.AppendLine(identation + "empty"); + } + } + catch (Exception ex) + { + sb.AppendLine(identation + $"Error: {ex.Message}"); + } + + sb.AppendLine("]"); + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/CollectionsConfigurator.cs b/ObjectPrinting/CollectionsConfigurator.cs new file mode 100644 index 000000000..93e418316 --- /dev/null +++ b/ObjectPrinting/CollectionsConfigurator.cs @@ -0,0 +1,41 @@ +using System; + +namespace ObjectPrinting; + +public class CollectionsConfigurator +{ + private readonly PrintingConfig _printingConfig; + private readonly PrintingConfigData _data; + private readonly Type _collectionType; + + public CollectionsConfigurator(PrintingConfig printingConfig, PrintingConfigData data, Type collectionType) + { + _printingConfig = printingConfig; + _data = data; + _collectionType = collectionType; + } + + public static implicit operator PrintingConfig(CollectionsConfigurator configurator) + { + return configurator._printingConfig; + } + + public CollectionsConfigurator WithMaxItems(int maxItems) + { + _data.CollectionMaxItems[_collectionType] = maxItems; + return this; + } + + public CollectionsConfigurator WithItemSerialization(Func itemSerializer) + { + _data.CollectionSerializers[_collectionType] = itemSerializer; + return this; + } + + public string PrintToString(TOwner obj) + { + return _printingConfig.PrintToString(obj); + } + + +} \ No newline at end of file diff --git a/ObjectPrinting/CollectionsConfiguratorExtensions.cs b/ObjectPrinting/CollectionsConfiguratorExtensions.cs new file mode 100644 index 000000000..b7164805f --- /dev/null +++ b/ObjectPrinting/CollectionsConfiguratorExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting; + +public static class CollectionsConfiguratorExtensions +{ + public static CollectionsConfigurator WithItemSerialization( + this CollectionsConfigurator configurator, + Func itemSerializer) + { + return configurator.WithItemSerialization(obj => + { + if (obj is TItem item) + return itemSerializer(item); + return obj?.ToString() ?? "null"; + }); + } + + public static CollectionsConfigurator Trim( + this CollectionsConfigurator configurator, + int start, int end) + { + return configurator.WithItemSerialization(str => + { + var s = str as string; + if (s == null) return "null"; + + var length = Math.Min(s.Length - start, end - start); + return length > 0 ? s.Substring(start, length) : string.Empty; + }); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs index 3c7867c32..7814d3302 100644 --- a/ObjectPrinting/ObjectPrinter.cs +++ b/ObjectPrinting/ObjectPrinter.cs @@ -6,5 +6,6 @@ public static PrintingConfig For() { return new PrintingConfig(); } + } } \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterExtension.cs b/ObjectPrinting/ObjectPrinterExtension.cs new file mode 100644 index 000000000..08fa923bd --- /dev/null +++ b/ObjectPrinting/ObjectPrinterExtension.cs @@ -0,0 +1,18 @@ +using System; + +namespace ObjectPrinting; + +public static class ObjectPrinterExtensions +{ + public static string PrintToString(this T obj) + { + return ObjectPrinter.For().PrintToString(obj); + } + + public static string PrintToString(this T obj, Action> config) + { + var printer = ObjectPrinter.For(); + config(printer); + return printer.PrintToString(obj); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392ff..ea98111e3 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/ObjectPrinting/ObjectSerializer.cs b/ObjectPrinting/ObjectSerializer.cs new file mode 100644 index 000000000..8dad5f5d8 --- /dev/null +++ b/ObjectPrinting/ObjectSerializer.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace ObjectPrinting +{ + internal class ObjectSerializer + { + private readonly PrintingConfigData _config; + private static readonly HashSet FinalTypes = new() + { + typeof(int), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(bool), typeof(decimal), + typeof(long), typeof(short), typeof(byte), typeof(char), typeof(Guid) + }; + + public ObjectSerializer(PrintingConfigData config) + { + _config = config; + } + + public string Serialize(object obj) + { + return PrintToString(obj, 0, new HashSet()); + } + + public string PrintToString(object obj, int nestingLevel, HashSet visitedObjects) + { + if (obj == null) + return "null" + Environment.NewLine; + + if (!visitedObjects.Add(obj)) + return $"Cyclic reference to {obj.GetType().Name}" + Environment.NewLine; + + try + { + var type = obj.GetType(); + + if (FinalTypes.Contains(type)) + return ApplySerialization(obj, type) + Environment.NewLine; + + if (CollectionSerializer.IsDictionary(type)) + return CollectionSerializer.SerializeDictionary(obj, nestingLevel, visitedObjects, _config); + + if (CollectionSerializer.IsCollection(type)) + return CollectionSerializer.SerializeCollection(obj, nestingLevel, visitedObjects, _config); + + return SerializeObject(obj, nestingLevel, visitedObjects); + } + finally + { + visitedObjects.Remove(obj); + } + } + + private string SerializeObject(object obj, int nestingLevel, HashSet visitedObjects) + { + 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()) + { + if (_config.ExcludedTypes.Contains(propertyInfo.PropertyType) || + _config.ExcludedProperties.Contains(propertyInfo.Name)) + continue; + + var propertyValue = propertyInfo.GetValue(obj); + var serializedValue = SerializeProperty(propertyInfo, propertyValue, nestingLevel, visitedObjects); + + sb.Append(identation + propertyInfo.Name + " = " + serializedValue); + } + + return sb.ToString(); + } + + private string SerializeProperty(PropertyInfo propertyInfo, object propertyValue, int nestingLevel, HashSet visitedObjects) + { + var propertyName = propertyInfo.Name; + var propertyType = propertyInfo.PropertyType; + + if (_config.PropertySerializers.TryGetValue(propertyName, out var propertySerializer)) + return propertySerializer(propertyValue) + Environment.NewLine; + + if (_config.PropertyCultures.TryGetValue(propertyName, out var culture) && propertyValue is IFormattable formattableProperty) + return formattableProperty.ToString(null, culture) + Environment.NewLine; + + if (_config.TypeSerializers.TryGetValue(propertyType, out var typeSerializer)) + return typeSerializer(propertyValue) + Environment.NewLine; + + if (_config.TypeCultures.ContainsKey(propertyType) && propertyValue is IFormattable formattable) + return formattable.ToString(null, _config.TypeCultures[propertyType]) + Environment.NewLine; + + return PrintToString(propertyValue, nestingLevel + 1, visitedObjects); + } + + private string ApplySerialization(object value, Type type) + { + if (value == null) + return "null"; + + if (_config.TypeSerializers.TryGetValue(type, out var serializer)) + return serializer(value); + + if (_config.TypeCultures.ContainsKey(type) && value is IFormattable formattable) + return formattable.ToString(null, _config.TypeCultures[type]); + + if (type == typeof(Guid)) + return value.ToString(); + + return value.ToString() ?? "null"; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e082117..cd32d767b 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,53 @@ using System; -using System.Linq; -using System.Text; +using System.Linq.Expressions; namespace ObjectPrinting { public class PrintingConfig { + private readonly PrintingConfigData _data = new(); + public string PrintToString(TOwner obj) { - return PrintToString(obj, 0); + var serializer = new ObjectSerializer(_data); + return serializer.Serialize(obj); + } + + public PrintingConfig ExcludeType() + { + _data.ExcludedTypes.Add(typeof(T)); + return this; } - private string PrintToString(object obj, int nestingLevel) + public TypeConfigurator ConfigureType() { - //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(); + return new TypeConfigurator(this, _data); + } + + public PropertyConfigurator ConfigureProperty( + Expression> propertySelector) + { + var memberExpression = propertySelector.Body as MemberExpression; + if (memberExpression == null) + throw new ArgumentException("Expression should be a property selector"); + + var propertyName = memberExpression.Member.Name; + return new PropertyConfigurator(this, _data, propertyName); + } + + public CollectionsConfigurator ConfigureCollection() + { + return new CollectionsConfigurator(this, _data, typeof(TCollection)); + } + + public PrintingConfig ExcludeProperty(Expression> propertySelector) + { + var memberExpression = propertySelector.Body as MemberExpression; + if (memberExpression == null) + throw new ArgumentException("Expression should be a property selector"); + + _data.ExcludedProperties.Add(memberExpression.Member.Name); + return this; } } } \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfigData.cs b/ObjectPrinting/PrintingConfigData.cs new file mode 100644 index 000000000..260104aae --- /dev/null +++ b/ObjectPrinting/PrintingConfigData.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace ObjectPrinting; + +public class PrintingConfigData +{ + public HashSet ExcludedTypes { get; } = new(); + public HashSet ExcludedProperties { get; } = new(); + public Dictionary> TypeSerializers { get; } = new(); + public Dictionary TypeCultures { get; } = new(); + public Dictionary> PropertySerializers { get; } = new(); + public Dictionary PropertyCultures { get; } = new(); + public Dictionary> CollectionSerializers { get; } = new(); + public Dictionary CollectionMaxItems { get; } = new(); +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyConfigurator.cs b/ObjectPrinting/PropertyConfigurator.cs new file mode 100644 index 000000000..651d8503b --- /dev/null +++ b/ObjectPrinting/PropertyConfigurator.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting; + +public class PropertyConfigurator +{ + private readonly PrintingConfig _printingConfig; + private readonly PrintingConfigData _data; + private readonly string _propertyName; + + public PropertyConfigurator(PrintingConfig printingConfig, PrintingConfigData data, string propertyName) + { + _printingConfig = printingConfig; + _data = data; + _propertyName = propertyName; + } + + public PrintingConfig WithSerialization(Func serializer) + { + _data.PropertySerializers[_propertyName] = obj => serializer((TProp)obj); + return _printingConfig; + } + + internal PropertyConfigurator WithInternalCulture(CultureInfo culture) + { + _data.PropertyCultures[_propertyName] = culture; + return this; + } +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyExtension.cs b/ObjectPrinting/PropertyExtension.cs new file mode 100644 index 000000000..6846accef --- /dev/null +++ b/ObjectPrinting/PropertyExtension.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting; + +public static class PropertyExtensions +{ + public static PrintingConfig WithCulture( + this PropertyConfigurator configurator, + CultureInfo culture) where TProp : struct, IFormattable + { + configurator.WithInternalCulture(culture); + return configurator.WithSerialization(value => value.ToString(null, culture)); + } + + public static PrintingConfig Trim( + this PropertyConfigurator configurator, + int start, int end) + { + return configurator.WithSerialization(str => + { + if (str == null) return "null"; + var length = Math.Min(str.Length - start, end - start); + return length > 0 ? str.Substring(start, length) : string.Empty; + }); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/BasicSerializationTests.cs b/ObjectPrinting/Tests/BasicSerializationTests.cs new file mode 100644 index 000000000..6c40459bf --- /dev/null +++ b/ObjectPrinting/Tests/BasicSerializationTests.cs @@ -0,0 +1,71 @@ +using System; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class BasicSerializationTests +{ + [Test] + public void ExcludeType_ShouldExcludeAllPropertiesOfType() + { + var person = new Person { Name = "Alex", Surname = "Ivanov",Age = 19, Height = 180.5, BirthDate = DateTime.Now }; + + var printer = ObjectPrinter.For() + .ExcludeType() + .ExcludeType(); + + var result = printer.PrintToString(person); + + result.Should().NotContain("Name").And.NotContain("BirthDate").And.NotContain("Surname") + .And.Contain("Age").And.Contain("Height"); + } + + [Test] + public void ExcludeProperty_ShouldExcludeSpecificProperty() + { + var person = new Person { Name = "Alex", Age = 19, Height = 180.5 }; + + var printer = ObjectPrinter.For() + .ExcludeProperty(p => p.Height) + .ExcludeProperty(p => p.Age); + + var result = printer.PrintToString(person); + + result.Should().Contain("Name") + .And.NotContain("Height").And.NotContain("Age"); + } + + [Test] + public void ConfigureType_WithCustomSerialization_ShouldApplyToAllPropertiesOfType() + { + var person = new Person { Age = 19, YearOfBirth = 2004, Height = 180 }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithSerialization(i => $"#{i}#") + .ConfigureType().WithSerialization(d => $"{d:0.0}cm"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Age = #19#") + .And.Contain("YearOfBirth = #2004#") + .And.Contain("Height = 180,0cm"); + } + + [Test] + public void ConfigureProperty_WithCustomSerialization_ShouldApplyToSpecificProperty() + { + var person = new Person { Name = "Alex", Age = 19, YearOfBirth = 2004 }; + + var printer = ObjectPrinter.For() + .ConfigureProperty(p => p.Age).WithSerialization(age => $"Возраст: {age}") + .ConfigureProperty(p => p.Name).WithSerialization(name => $"Имя: {name}"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Age = Возраст: 19") + .And.Contain("Name = Имя: Alex") + .And.Contain("YearOfBirth = 2004"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/CircularReferenceTests.cs b/ObjectPrinting/Tests/CircularReferenceTests.cs new file mode 100644 index 000000000..9a4a12f3b --- /dev/null +++ b/ObjectPrinting/Tests/CircularReferenceTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CircularReferenceTests +{ + [Test] + public void PrintToString_WithCircularReference_ShouldNotStackOverflow() + { + var person1 = new Person { Name = "Person1" }; + var person2 = new Person { Name = "Person2" }; + + person1.Parent = person2; + person2.Parent = person1; + + var printer = ObjectPrinter.For(); + + Action act = () => printer.PrintToString(person1); + + act.Should().NotThrow(); + } + + [Test] + public void PrintToString_WithSelfReference_ShouldHandleCorrectly() + { + var person = new Person { Name = "SelfReferencing" }; + person.Parent = person; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Cyclic reference").And.Contain("SelfReferencing"); + } + + [Test] + public void PrintToString_WithComplexCircularReference_ShouldHandleCorrectly() + { + var company = new Company { Name = "Tech Corp" }; + var department = new Department { Name = "Development", Company = company }; + var employee = new Employee { Name = "John", Department = department }; + + company.Departments = new List { department }; + department.Employees = new List { employee }; + employee.Manager = employee; + + var printer = ObjectPrinter.For(); + + Action act = () => printer.PrintToString(company); + + act.Should().NotThrow(); + } + + [Test] + public void CircularReference_WithConfiguration_ShouldStillWork() + { + var person1 = new Person { Name = "Person1", Age = 30 }; + var person2 = new Person { Name = "Person2", Age = 25 }; + + person1.Parent = person2; + person2.Parent = person1; + + var printer = ObjectPrinter.For() + .ConfigureType().WithSerialization(i => $"{i} years") + .ConfigureProperty(p => p.Name).Trim(0, 6) + .ExcludeProperty(p => p.BirthDate); + + Action act = () => printer.PrintToString(person1); + + act.Should().NotThrow(); + } +} + +public class Company +{ + public string Name { get; set; } + public List Departments { get; set; } +} + +public class Department +{ + public string Name { get; set; } + public Company Company { get; set; } + public List Employees { get; set; } +} + +public class Employee +{ + public string Name { get; set; } + public Department Department { get; set; } + public Employee Manager { get; set; } + public decimal Salary { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/CollectionConfigurationTests.cs b/ObjectPrinting/Tests/CollectionConfigurationTests.cs new file mode 100644 index 000000000..b5aae33ad --- /dev/null +++ b/ObjectPrinting/Tests/CollectionConfigurationTests.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CollectionConfigurationTests +{ + [Test] + public void ConfigureCollection_WithMaxItems_ShouldLimitCollectionOutput() + { + var person = new Person + { + Name = "Alex", + Friends = new List { "John", "Mike", "Sarah", "Tom", "Anna" } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection>() + .WithMaxItems(3); + + var result = printer.PrintToString(person); + + result.Should().Contain("John") + .And.Contain("Mike") + .And.Contain("Sarah") + .And.Contain("... and 2 more items") + .And.NotContain("Tom") + .And.NotContain("Anna"); + } + + [Test] + public void ConfigureCollection_WithItemSerialization_ShouldApplyToCollectionItems() + { + var person = new Person + { + Name = "Alex", + Scores = new List { 95, 87, 92 } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection>() + .WithItemSerialization(score => $"Score: {score}"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Score: 95") + .And.Contain("Score: 87") + .And.Contain("Score: 92"); + } + + [Test] + public void ConfigureCollection_TrimStrings_ShouldTrimStringItems() + { + var person = new Person + { + Name = "Alex", + Friends = new List { "Alexander", "Michael", "Jennifer" } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection>() + .Trim(0, 4); + + var result = printer.PrintToString(person); + + result.Should().Contain("Alex") + .And.Contain("Mich") + .And.Contain("Jenn"); + } + + [Test] + public void ConfigureCollection_WithCulture_ShouldApplyCultureToNumericItems() + { + var person = new Person + { + Name = "Alex", + Prices = new List { 19.99m, 29.50m, 5.75m } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection>() + .WithItemSerialization(price => ((decimal)price).ToString("C", new CultureInfo("en-US"))); + + var result = printer.PrintToString(person); + + result.Should().Contain("$19.99") + .And.Contain("$29.50") + .And.Contain("$5.75"); + } + + [Test] + public void ConfigureCollection_ForArrays_ShouldWork() + { + var person = new Person + { + Name = "Alex", + ScoresArray = new[] { 95, 87, 92 } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection() + .WithMaxItems(2); + + var result = printer.PrintToString(person); + + result.Should().Contain("95").And.Contain("87") + .And.Contain("... and 1 more items") + .And.NotContain("92"); + } + + [Test] + public void ConfigureCollection_ForDictionaries_ShouldWork() + { + var person = new Person + { + Name = "Alex", + Grades = new Dictionary + { + ["Math"] = 95, + ["Science"] = 87, + ["History"] = 92, + ["Art"] = 78 + } + }; + + var printer = ObjectPrinter.For() + .ConfigureCollection>() + .WithMaxItems(2); + + var result = printer.PrintToString(person); + + result.Should().Contain("Math: 95").And.Contain("Science: 87") + .And.Contain("... and 2 more items") + .And.NotContain("History").And.NotContain("Art"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/CollectionSerializationTests.cs b/ObjectPrinting/Tests/CollectionSerializationTests.cs new file mode 100644 index 000000000..8a0aaea7b --- /dev/null +++ b/ObjectPrinting/Tests/CollectionSerializationTests.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CollectionSerializationTests +{ + [Test] + public void PrintToString_WithIntArray_ShouldSerializeCorrectly() + { + var person = new Person + { + Name = "Alex", + Scores = new List() { 95, 87, 92 } + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Scores") + .And.Contain("[") + .And.Contain("95") + .And.Contain("87") + .And.Contain("92"); + } + + [Test] + public void PrintToString_WithStringList_ShouldSerializeCorrectly() + { + var person = new Person + { + Name = "Alex", + Friends = new List { "John", "Mike", "Sarah" } + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Friends") + .And.Contain("[") + .And.Contain("John") + .And.Contain("Mike") + .And.Contain("Sarah"); + } + + [Test] + public void PrintToString_WithEmptyCollection_ShouldHandleCorrectly() + { + var person = new Person + { + Name = "Alex", + Friends = new List(), + Scores = new List() + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Friends").And.Contain("Scores") + .And.NotContain("John"); + } + + [Test] + public void PrintToString_WithNestedLists_ShouldSerializeCorrectly() + { + var person = new Person + { + Name = "Alex", + Matrix = new List> + { + new() { 1, 2, 3 }, + new() { 4, 5, 6 } + } + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Matrix") + .And.Contain("1").And.Contain("2").And.Contain("3") + .And.Contain("4").And.Contain("5").And.Contain("6"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/CombinedScenariosTests.cs b/ObjectPrinting/Tests/CombinedScenariosTests.cs new file mode 100644 index 000000000..eb17174b8 --- /dev/null +++ b/ObjectPrinting/Tests/CombinedScenariosTests.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CombinedScenariosTests +{ + [Test] + public void CombinedConfigurations_ShouldWorkTogether() + { + var person = new Person + { + Name = "Alexander", + Age = 25, + Height = 180.5, + YearOfBirth = 1998, + Friends = new List { "John", "Michael", "Sarah" } + }; + + var printer = ObjectPrinter.For() + .ExcludeType() + .ConfigureType().WithSerialization(i => $"{i} years") + .ConfigureType().WithCulture(CultureInfo.InvariantCulture) + .ConfigureProperty(p => p.Age).WithSerialization(age => $"Возраст: {age}") + .ConfigureCollection>() + .Trim(0, 4); + + var result = printer.PrintToString(person); + + result.Should().NotContain("Alexander") + .And.Contain("Age = Возраст: 25") + .And.Contain("YearOfBirth = 1998 years") + .And.Contain("John") + .And.Contain("Mich"); + } + + [Test] + public void PropertySerialization_ShouldHavePriorityOverTypeSerialization() + { + var person = new Person { Name = "Alex", Age = 25, YearOfBirth = 1998 }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithSerialization(i => $"{i} years") + .ConfigureProperty(p => p.Age).WithSerialization(age => $"Возраст: {age}"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Age = Возраст: 25") + .And.Contain("YearOfBirth = 1998 years"); + } + + [Test] + public void CollectionConfig_ShouldOverrideTypeConfig() + { + var person = new Person + { + Name = "Alex", + Scores = new List { 95, 87, 92 }, + YearOfBirth = 1998 + }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithSerialization(i => $"#{i}#") + .ConfigureCollection>() + .WithItemSerialization(score => $"Score: {score}"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Score: 95") + .And.Contain("Score: 87") + .And.Contain("Score: 92") + .And.Contain("YearOfBirth = #1998#"); + } + + [Test] + public void ComplexObject_WithAllFeatures_ShouldSerializeCorrectly() + { + var company = new Company + { + Name = "Tech Corp", + Departments = new List + { + new Department + { + Name = "Development", + Employees = new List + { + new Employee { Name = "John", Salary = 50000.0m }, + new Employee { Name = "Sarah", Salary = 60000.0m } + } + } + } + }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithCulture(new CultureInfo("en-US")) + .ConfigureProperty(p => p.Name).WithSerialization(name => $"Company: {name}") + .ConfigureCollection>() + .WithItemSerialization(emp => + { + if (emp is Employee employee) + { + return $"{employee.Name} (${employee.Salary.ToString("F2", CultureInfo.InvariantCulture)})"; + } + return emp?.ToString() ?? "null"; + }); + + var result = printer.PrintToString(company); + + result.Should().Contain("Company: Tech Corp") + .And.Contain("John ($50000.00)") + .And.Contain("Sarah ($60000.00)"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/CultureSerializationTests.cs b/ObjectPrinting/Tests/CultureSerializationTests.cs new file mode 100644 index 000000000..551a8a039 --- /dev/null +++ b/ObjectPrinting/Tests/CultureSerializationTests.cs @@ -0,0 +1,68 @@ +using System.Globalization; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class CultureSerializationTests +{ + [Test] + public void ConfigureType_WithCulture_ShouldApplyCultureToAllNumericProperties() + { + var person = new Person { Height = 180.5, Weight = 75.3, Salary = 1000.99m }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithCulture(new CultureInfo("de-DE")) + .ConfigureType().WithCulture(new CultureInfo("fr-FR")); + + var result = printer.PrintToString(person); + + result.Should().Contain("180,5").And.Contain("75,3").And.Contain("1000,99"); + } + + [Test] + public void ConfigureProperty_WithCulture_ShouldApplyCultureOnlyToSpecificProperty() + { + var person = new Person { Height = 180.5, Weight = 75.3 }; + + var printer = ObjectPrinter.For() + .ConfigureProperty(p => p.Height).WithCulture(new CultureInfo("de-DE")) + .ConfigureType().WithCulture(new CultureInfo("en-US")); + + var result = printer.PrintToString(person); + + result.Should().Contain("Height = 180,5") + .And.Contain("Weight = 75.3"); + } + + [Test] + public void Culture_ShouldWorkWithDifferentNumericTypes() + { + var obj = new NumericTypes + { + IntValue = 1000, + DoubleValue = 1234.56, + DecimalValue = 789.12m, + FloatValue = 456.78f + }; + + var printer = ObjectPrinter.For() + .ConfigureType().WithCulture(new CultureInfo("de-DE")) + .ConfigureType().WithCulture(new CultureInfo("fr-FR")) + .ConfigureType().WithCulture(new CultureInfo("es-ES")) + .ConfigureType().WithCulture(new CultureInfo("ru-RU")); + + var result = printer.PrintToString(obj); + + result.Should().Contain("1000").And.Contain("1234,56").And.Contain("789,12").And.Contain("456,78"); + } +} + +public class NumericTypes +{ + public int IntValue { get; set; } + public double DoubleValue { get; set; } + public decimal DecimalValue { get; set; } + public float FloatValue { get; set; } +} diff --git a/ObjectPrinting/Tests/DictionarySerializationTests.cs b/ObjectPrinting/Tests/DictionarySerializationTests.cs new file mode 100644 index 000000000..f90c8c530 --- /dev/null +++ b/ObjectPrinting/Tests/DictionarySerializationTests.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class DictionarySerializationTests +{ + [Test] + public void PrintToString_WithStringIntDictionary_ShouldSerializeCorrectly() + { + var person = new Person + { + Name = "Alex", + Grades = new Dictionary + { + ["Math"] = 95, + ["Science"] = 87 + } + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Grades") + .And.Contain("{") + .And.Contain("Math: 95") + .And.Contain("Science: 87"); + } + + [Test] + public void PrintToString_WithComplexValueDictionary_ShouldSerializeCorrectly() + { + var person = new Person + { + Name = "Alex", + SubjectDetails = new Dictionary + { + ["Math"] = new() { Score = 95, Teacher = "Mr. Smith" }, + ["Science"] = new() { Score = 87, Teacher = "Ms. Johnson" } + } + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("SubjectDetails") + .And.Contain("Math") + .And.Contain("Science") + .And.Contain("Mr. Smith") + .And.Contain("Ms. Johnson"); + } + + [Test] + public void PrintToString_WithEmptyDictionary_ShouldHandleCorrectly() + { + var person = new Person + { + Name = "Alex", + Grades = new Dictionary() + }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Grades").And.NotContain("Math"); + } +} + +public class Subject +{ + public int Score { get; set; } + public string Teacher { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/EdgeCasesTests.cs b/ObjectPrinting/Tests/EdgeCasesTests.cs new file mode 100644 index 000000000..c59a9a4b7 --- /dev/null +++ b/ObjectPrinting/Tests/EdgeCasesTests.cs @@ -0,0 +1,57 @@ +using System; +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class EdgeCasesTests +{ + [Test] + public void PrintToString_WithNullObject_ShouldReturnNull() + { + Person person = null; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Be("null" + Environment.NewLine); + } + + [Test] + public void PrintToString_WithNullProperties_ShouldHandleCorrectly() + { + var person = new Person { Name = null, Friends = null }; + + var printer = ObjectPrinter.For(); + + var result = printer.PrintToString(person); + + result.Should().Contain("Name = null").And.Contain("Friends = null"); + } + + [Test] + public void ConfigureNonExistentProperty_ShouldThrowException() + { + var printer = ObjectPrinter.For(); + + Action act = () => printer.ConfigureProperty(p => "invalid"); + + act.Should().Throw(); + } + + [Test] + public void MultipleConfigurations_ForSameProperty_ShouldUseLastOne() + { + var person = new Person { Age = 25 }; + + var printer = ObjectPrinter.For() + .ConfigureProperty(p => p.Age).WithSerialization(age => $"First: {age}") + .ConfigureProperty(p => p.Age).WithSerialization(age => $"Second: {age}"); + + var result = printer.PrintToString(person); + + result.Should().Contain("Second: 25").And.NotContain("First: 25"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445c..ef823c738 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +using System; +using System.Globalization; +using NUnit.Framework; namespace ObjectPrinting.Tests { @@ -8,20 +10,40 @@ public class ObjectPrinterAcceptanceTests [Test] public void Demo() { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person { Name = "Alex", Age = 19, Height = 19.8}; - var printer = ObjectPrinter.For(); + var printer = ObjectPrinter.For() //1. Исключить из сериализации свойства определенного типа - //2. Указать альтернативный способ сериализации для определенного типа - //3. Для числовых типов указать культуру - //4. Настроить сериализацию конкретного свойства - //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства + .ExcludeType() + // //2. Указать альтернативный способ сериализации для определенного типа + //.Configure().Print(x => ...) + .ConfigureType().WithSerialization(i => i + ".000") + // //3. Для числовых типов указать культуру + .ConfigureType().WithCulture(new CultureInfo("en-US")) + // //4. Настроить сериализацию конкретного свойства + .ConfigureProperty(p => p.Age).WithSerialization(age => $"age: {age}") + // //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .ConfigureProperty(p => p.Name).Trim(0, 5) + // //6. Исключить из сериализации конкретного свойства + .ExcludeProperty(p => p.Height) + ; string s1 = printer.PrintToString(person); - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию - //8. ...с конфигурированием + // 7. Синтаксический сахар - сериализация по-умолчанию + string defaultResult = person.PrintToString(); + + // 8. ...с конфигурированием - компактный синтаксис + string configuredResult = person.PrintToString(config => config + .ExcludeType() + .ConfigureType().WithSerialization(i => i + ".000") + .ConfigureType().WithCulture(new CultureInfo("en-US")) + .ConfigureProperty(p => p.Age).WithSerialization(age => $"age: {age}") + .ConfigureProperty(p => p.Name).Trim(0, 5) + .ExcludeProperty(p => p.Height) + ); + + } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index f95559554..8a1217d10 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -1,12 +1,26 @@ using System; +using System.Collections.Generic; namespace ObjectPrinting.Tests { public class Person { - public Guid Id { get; set; } public string Name { get; set; } - public double Height { get; set; } + public string Surname { get; set; } public int Age { get; set; } + public double Height { get; set; } + public double Weight { get; set; } + public int YearOfBirth { get; set; } + public Person Parent { get; set; } + public DateTime BirthDate { get; set; } + public List Friends { get; set; } + public List Scores { get; set; } + public List Prices { get; set; } + public Dictionary Grades { get; set; } + public decimal Salary { get; set; } + public string Description { get; set; } + public List> Matrix { get; set; } + public Dictionary SubjectDetails { get; set; } + public int[] ScoresArray { get; set; } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/StringTrimmingTests.cs b/ObjectPrinting/Tests/StringTrimmingTests.cs new file mode 100644 index 000000000..60d31f513 --- /dev/null +++ b/ObjectPrinting/Tests/StringTrimmingTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class StringTrimmingTests +{ + [Test] + public void ConfigureType_Trim_ShouldTrimAllStringProperties() + { + var person = new Person { Name = "Alexander", Description = "Very long description" }; + + var printer = ObjectPrinter.For() + .ConfigureType().Trim(0, 5); + + var result = printer.PrintToString(person); + + result.Should().Contain("Name = Alex").And.Contain("Description = Very "); + } + + [Test] + public void ConfigureProperty_Trim_ShouldTrimOnlySpecificProperty() + { + var person = new Person { Name = "Alexander", Description = "Very long description" }; + + var printer = ObjectPrinter.For() + .ConfigureProperty(p => p.Name).Trim(0, 4) + .ConfigureProperty(p => p.Description).Trim(0, 7); + + var result = printer.PrintToString(person); + + result.Should().Contain("Name = Alex").And.Contain("Description = Very lo"); + } + + [Test] + public void Trim_WithVariousParameters_ShouldWorkCorrectly() + { + var person = new Person { Name = "Alexander" }; + + var printer1 = ObjectPrinter.For() + .ConfigureProperty(p => p.Name).Trim(2, 6); + var result1 = printer1.PrintToString(person); + + var printer2 = ObjectPrinter.For() + .ConfigureProperty(p => p.Name).Trim(0, 100); + var result2 = printer2.PrintToString(person); + + result1.Should().Contain("Name = exa"); + result2.Should().Contain("Name = Alexander"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/TypeConfigurator.cs b/ObjectPrinting/TypeConfigurator.cs new file mode 100644 index 000000000..aa0f67191 --- /dev/null +++ b/ObjectPrinting/TypeConfigurator.cs @@ -0,0 +1,21 @@ +using System; + +namespace ObjectPrinting; + +public class TypeConfigurator +{ + private readonly PrintingConfig _printingConfig; + private readonly PrintingConfigData _data; + + public TypeConfigurator(PrintingConfig printingConfig, PrintingConfigData data) + { + _printingConfig = printingConfig; + _data = data; + } + + public PrintingConfig WithSerialization(Func serializer) + { + _data.TypeSerializers[typeof(T)] = obj => serializer((T)obj); + return _printingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/TypeConfiguratorExtensions.cs b/ObjectPrinting/TypeConfiguratorExtensions.cs new file mode 100644 index 000000000..eaa361051 --- /dev/null +++ b/ObjectPrinting/TypeConfiguratorExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting; + +public static class TypeConfiguratorExtensions +{ + public static PrintingConfig WithCulture( + this TypeConfigurator configurator, + CultureInfo culture) where T : struct, IFormattable + { + return configurator.WithSerialization(value => value.ToString(null, culture)); + } + + public static PrintingConfig Trim( + this TypeConfigurator configurator, + int start, int end) + { + return configurator.WithSerialization(str => + { + if (str == null) return "null"; + var length = Math.Min(str.Length - start, end - start); + return length > 0 ? str.Substring(start, length) : string.Empty; + }); + } +} \ 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