diff --git a/ObjectPrinting/Extensions/CultureExtension.cs b/ObjectPrinting/Extensions/CultureExtension.cs new file mode 100644 index 00000000..ad3deecf --- /dev/null +++ b/ObjectPrinting/Extensions/CultureExtension.cs @@ -0,0 +1,16 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting.Extensions; + +public static class CultureExtension +{ + public static PrintingConfig UseCulture( + this PropertyPrintingConfiguration propertyStringConfiguration, + CultureInfo culture) where TPropType : IFormattable + { + propertyStringConfiguration.ParentConfig.AddTypeSerializer(t => ((TPropType)t).ToString(null, culture)); + + return propertyStringConfiguration.ParentConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/ObjectPrinterExtension.cs b/ObjectPrinting/Extensions/ObjectPrinterExtension.cs new file mode 100644 index 00000000..cd5f1544 --- /dev/null +++ b/ObjectPrinting/Extensions/ObjectPrinterExtension.cs @@ -0,0 +1,12 @@ +using System; + +namespace ObjectPrinting.Extensions; + +public static class ObjectPrinterExtension +{ + 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/Extensions/StringPrintingConfigurationExtension.cs b/ObjectPrinting/Extensions/StringPrintingConfigurationExtension.cs new file mode 100644 index 00000000..b1a7a089 --- /dev/null +++ b/ObjectPrinting/Extensions/StringPrintingConfigurationExtension.cs @@ -0,0 +1,13 @@ +namespace ObjectPrinting.Extensions; + +public static class StringPrintingConfigurationExtension +{ + public static PrintingConfig TrimmedToLength( + this PropertyPrintingConfiguration propertyStringConfiguration, int length) + { + propertyStringConfiguration.ParentConfig + .AddStringPropertyTrim(propertyStringConfiguration.PropertyMemberInfo.Name, length); + + return propertyStringConfiguration.ParentConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392f..a5c71017 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,6 +5,7 @@ + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..2f16a9b1 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,180 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text; namespace ObjectPrinting { public class PrintingConfig { + private readonly HashSet excludedTypes = new(); + private readonly HashSet excludedProperties = new(); + private readonly Dictionary> typeSerializers = new(); + private readonly Dictionary> propertySerializers = new(); + private readonly Dictionary propertyTrim = new(); + private int maxNestingLevel = 5; + + public int MaxNestingLevel + { + get => maxNestingLevel; + private set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), value, "The maxNestingDepth value must be positive."); + } + maxNestingLevel = value; + } + } + + private readonly HashSet finalTypes = + [ + typeof(bool), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), + typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), + typeof(double), typeof(decimal), typeof(string), typeof(DateTime), typeof(TimeSpan), typeof(Guid) + ]; + + public PrintingConfig Exclude() + { + excludedTypes.Add(typeof(TPropType)); + return this; + } + + public PropertyPrintingConfiguration Printing( + Expression> propertySelector) + { + var memberInfo = GetPropertyName(propertySelector); + return new PropertyPrintingConfiguration(this, memberInfo); + } + + public PropertyPrintingConfiguration Printing() + { + return new PropertyPrintingConfiguration(this, null); + } + + public PrintingConfig Exclude(Expression> propertySelector) + { + var propertyName = GetPropertyName(propertySelector); + excludedProperties.Add(propertyName); + return this; + } + public string PrintToString(TOwner obj) { return PrintToString(obj, 0); } + private string PrintToString(object obj, int nestingLevel) { - //TODO apply configurations + if (nestingLevel > MaxNestingLevel) + return "Maximum serialization depth has been reached"; + if (obj == null) - return "null" + Environment.NewLine; + return "null"; + + var type = obj.GetType(); + + if (excludedTypes.Contains(type)) + return string.Empty; - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; + return obj.ToString(); + + if (typeSerializers.TryGetValue(type, out var serializer)) + return serializer(obj).ToString(); + + if (obj is ICollection collection) + return SerializeCollection(collection, nestingLevel); 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)); + var propertyValue = propertyInfo.GetValue(obj); + var propertyType = propertyInfo.PropertyType; + var propertyName = propertyInfo.Name; + + if (excludedProperties.Contains(propertyInfo)) + continue; + if (excludedTypes.Contains(propertyType)) + continue; + + if (typeSerializers.TryGetValue(propertyType, out var typeSerializer)) + { + sb.AppendLine($"{identation}{propertyName} = {typeSerializer(propertyValue)}"); + continue; + } + + if (propertySerializers.TryGetValue(propertyName, out var propertySerializer)) + { + sb.AppendLine($"{identation}{propertyName} = {propertySerializer(propertyValue)}"); + continue; + } + + if (propertyTrim.TryGetValue(propertyName, out var toTrimLength) && propertyValue is string stringValue) + { + sb.AppendLine($"{identation}{propertyName} = {stringValue.Substring(0, Math.Min(toTrimLength, stringValue.Length))}"); + continue; + } + + sb.AppendLine($"{identation}{propertyName} = {PrintToString(propertyValue, nestingLevel + 1)}"); } return sb.ToString(); } + + private string SerializeCollection(ICollection collection, int nestingLevel) + { + var identation = new string('\t', nestingLevel + 1); + var sb = new StringBuilder(); + + sb.AppendLine("["); + + if (collection is IDictionary dictionary) + { + foreach (var key in dictionary.Keys) + { + sb.Append($"{identation}{{{PrintToString(key, nestingLevel)}: " + + $"{PrintToString(dictionary[key], nestingLevel + 1)}}}\n"); + } + } + else + { + foreach (var value in collection) + { + sb.Append('\t', nestingLevel + 1); + sb.Append(PrintToString(value, nestingLevel + 1)); + } + } + + sb.AppendLine($"{identation}]"); + return sb.ToString(); + } + + private MemberInfo GetPropertyName(Expression> propertySelector) + { + if (propertySelector.Body is MemberExpression memberExpression) + return memberExpression.Member; + + if (propertySelector.Body is UnaryExpression unaryExpression && unaryExpression.Operand is MemberExpression operand) + return operand.Member; + + throw new ArgumentException("Invalid property selector expression"); + } + + internal void AddPropertySerializer(string propertyName, Func serializer) => + propertySerializers[propertyName] = serializer; + + internal void AddTypeSerializer(Func serializer) => + typeSerializers[typeof(TPropType)] = serializer; + + internal void AddStringPropertyTrim(string propertyName, int maxLength) => + propertyTrim[propertyName] = maxLength; } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfiguration.cs b/ObjectPrinting/PropertyPrintingConfiguration.cs new file mode 100644 index 00000000..44d068b5 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfiguration.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting; + +public class PropertyPrintingConfiguration +{ + private readonly PrintingConfig parentConfig; + private readonly MemberInfo? propertyMemberInfo; + + internal PrintingConfig ParentConfig => parentConfig; + internal MemberInfo? PropertyMemberInfo => propertyMemberInfo; + + public PropertyPrintingConfiguration(PrintingConfig parentConfig, MemberInfo? propertyMemberInfo) + { + this.parentConfig = parentConfig; + this.propertyMemberInfo = propertyMemberInfo; + } + + public PrintingConfig Using(Func printingMethod) + { + if (propertyMemberInfo == null) + { + parentConfig.AddTypeSerializer(obj => printingMethod((TPropType)obj)); + } + else + { + parentConfig.AddPropertySerializer(propertyMemberInfo.Name, obj => printingMethod((TPropType)obj)); + } + + return parentConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445..36037059 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,4 +1,7 @@ using NUnit.Framework; +using System; +using System.Globalization; +using ObjectPrinting.Extensions; namespace ObjectPrinting.Tests { @@ -8,20 +11,32 @@ public class ObjectPrinterAcceptanceTests [Test] public void Demo() { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person { Age = 19, Name = "Alex", Height = 173.65, Id = new Guid() }; - var printer = ObjectPrinter.For(); + var printer = ObjectPrinter.For() //1. Исключить из сериализации свойства определенного типа + .Exclude() //2. Указать альтернативный способ сериализации для определенного типа + .Printing().Using(i => i.ToString("X")) //3. Для числовых типов указать культуру + .Printing().UseCulture(CultureInfo.InvariantCulture) //4. Настроить сериализацию конкретного свойства + .Printing(x => x.Name).Using(p => $"---{p}---") //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) + .Printing(p => p.Name).TrimmedToLength(10) //6. Исключить из сериализации конкретного свойства - + .Exclude(p => p.Age); + string s1 = printer.PrintToString(person); + Console.WriteLine(s1); + + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); + Console.WriteLine(s2); - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию //8. ...с конфигурированием + var s3 = person.PrintToString(p => p.Exclude(x => x.Age)); + Console.WriteLine(s3); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterTests.cs b/ObjectPrinting/Tests/ObjectPrinterTests.cs new file mode 100644 index 00000000..930a4d49 --- /dev/null +++ b/ObjectPrinting/Tests/ObjectPrinterTests.cs @@ -0,0 +1,242 @@ +using NUnit.Framework; +using System; +using FluentAssertions; +using System.Globalization; +using ObjectPrinting.Extensions; +using System.Collections.Generic; + +namespace ObjectPrinting.Tests; + +[TestFixture] +public class ObjectPrinterTests +{ + private Person person; + + [SetUp] + public void SetUp() + { + person = new Person + { + Age = 19, + Name = "Alex", + Height = 173.65, + Id = new Guid(), + DateOfBirth = new DateTime(1978, 6, 19) + }; + } + + [Test] + public void PrintingConfig_Exclude_ShouldExcludeGivenType() + { + var printer = ObjectPrinter.For() + .Exclude(); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Id)} = {person.Id}"); + } + + [Test] + public void PrintingConfig_ExcludeAndAddSerialization_ShouldExcludeGivenType() + { + var printer = ObjectPrinter.For() + .Exclude() + .Printing().Using(_ => "XXX"); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Name)} = XXX"); + } + + [Test] + public void PrintingConfig_AddSerializationAndExclude_ShouldExcludeGivenType() + { + var printer = ObjectPrinter.For() + .Printing().Using(_ => "XXX") + .Exclude(); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Name)} = XXX"); + } + + [Test] + public void PrintingConfig_ExcludeCollection_ShouldExcludeGivenType() + { + var printer = ObjectPrinter.For() + .Exclude>(); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Friends)}"); + } + + [Test] + public void PrintingConfig_ExcludeSomeTypes_ShouldExcludeGivenTypes() + { + var printer = ObjectPrinter.For() + .Exclude() + .Exclude(); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Id)} = {person.Id}"); + result.Should().NotContain($"{nameof(person.Age)} = {person.Age}"); + } + + [Test] + public void PrintingConfig_ExcludeTypeSeveralTimes_ShouldExcludeGivenType() + { + var printer = ObjectPrinter.For() + .Exclude() + .Exclude(); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Id)} = {person.Id}"); + } + + [Test] + public void PrintingConfig_CorrectWorksWithTime() + { + var printer = ObjectPrinter.For() + .Printing(p => p.DateOfBirth) + .Using(x => x.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)); + + var result = printer.PrintToString(person); + + result.Should().Contain($"{nameof(person.DateOfBirth)} = 19/06/1978"); + } + + [Test] + public void PrintingConfig_Exclude_ShouldExcludeGivenProperty() + { + var printer = ObjectPrinter.For() + .Exclude(p => p.Name); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Name)} = {person.Name}"); + } + + [Test] + public void PrintingConfig_PrintingType_ShouldUseCustomMethod() + { + const string printingType = "int"; + + var printer = ObjectPrinter.For() + .Printing() + .Using(_ => printingType); + + var result = printer.PrintToString(person); + + result.Should().Contain($"{nameof(person.Age)} = {printingType}"); + } + + [Test] + public void PrintingConfig_PrintingUseCulture_ShouldUseGivenCulture() + { + var culture = CultureInfo.CreateSpecificCulture("fr-FR"); + + var printer = ObjectPrinter.For() + .Printing() + .UseCulture(culture); + + var result = printer.PrintToString(person); + + result.Should().Contain($"{nameof(person.Age)} = {person.Age.ToString(culture)}"); + } + + [Test] + public void PrintingConfig_PrintingProperty_ShouldUseCustomMethod() + { + const string printingProperty = "Id"; + + var printer = ObjectPrinter.For() + .Printing(p => p.Id) + .Using(_ => printingProperty); + + var result = printer.PrintToString(person); + + result.Should().Contain($"{nameof(person.Id)} = {printingProperty}"); + } + + [Test] + public void PrintingConfig_PrintingTrim_ShouldReturnTrimmedValue() + { + const int trimLength = 4; + + var printer = ObjectPrinter.For() + .Printing(p => p.Name) + .TrimmedToLength(trimLength); + + var result = printer.PrintToString(person); + + result.Should().Contain($"{nameof(person.Name)} = {person.Name[..trimLength]}"); + } + + [Test] + public void PrintingConfig_ExcludeField_ShouldExcludeGivenProperty() + { + var printer = ObjectPrinter.For() + .Exclude(p => p.Age); + + var result = printer.PrintToString(person); + + result.Should().NotContain($"{nameof(person.Age)} = {person.Age}"); + } + + [Test] + public void PrintingConfig_PrintCycledObject_ShouldNotFail() + { + person.Friend = person; + + var result = person.PrintToString(); + + result.Should().Contain("Maximum serialization depth has been reached"); + } + + [Test] + public void PrintToString_PrintClassWithList_ShouldSerializeList() + { + var friends = new List { new(), new() }; + + SimplePerson simplePerson = new SimplePerson() { Name = "Alex", Id = new Guid() }; + + simplePerson.Friends = friends; + + var result = simplePerson.PrintToString(); + + result.Should().Contain("["); + result.Should().Contain("]"); + for (var i = 0; i < friends.Count; i++) + { + var friend = friends[i]; + + result.Should().Contain($"\t\t\t{nameof(friend.Id)} = {friend.Id}"); + result.Should().Contain($"\t\t\t{nameof(friend.Name)} = {friend.Name}"); + } + } + + [Test] + public void PrintToString_PrintClassWithDictionary_ShouldSerializeDictionary() + { + var neighbours = new Dictionary + { { 12, new SimplePerson() }, { 19, new SimplePerson() } }; + + SimplePerson simplePerson = new SimplePerson() { Name = "Alex", Id = new Guid() }; + + simplePerson.Neighbours = neighbours; + + var result = simplePerson.PrintToString(); + + foreach (var key in neighbours.Keys) + { + var neighbour = neighbours[key]; + + result.Should().Contain($"\t\t{{{key}: SimplePerson"); + result.Should().Contain($"\t\t\t{nameof(neighbour.Id)} = {neighbour.Id}"); + result.Should().Contain($"\t\t\t{nameof(neighbour.Name)} = {neighbour.Name}"); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index f9555955..eac9e539 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -1,4 +1,6 @@ -using System; +using FluentAssertions.Equivalency; +using System; +using System.Collections.Generic; namespace ObjectPrinting.Tests { @@ -8,5 +10,11 @@ public class Person public string Name { get; set; } public double Height { get; set; } public int Age { get; set; } + public DateTime DateOfBirth { get; set; } + public Person? Friend { get; set; } + + public List? Friends { get; set; } + + public Dictionary? Neighbours { get; set; } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/SimplePerson.cs b/ObjectPrinting/Tests/SimplePerson.cs new file mode 100644 index 00000000..a744d7f9 --- /dev/null +++ b/ObjectPrinting/Tests/SimplePerson.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting.Tests; + +public class SimplePerson +{ + public Guid Id { get; set; } + public string Name { get; set; } + + public List? Friends { get; set; } + public Dictionary? Neighbours { get; set; } +} \ No newline at end of file diff --git a/ObjectPrinting/Solved/ObjectExtensions.cs b/Solved/ObjectExtensions.cs similarity index 100% rename from ObjectPrinting/Solved/ObjectExtensions.cs rename to Solved/ObjectExtensions.cs diff --git a/ObjectPrinting/Solved/ObjectPrinter.cs b/Solved/ObjectPrinter.cs similarity index 100% rename from ObjectPrinting/Solved/ObjectPrinter.cs rename to Solved/ObjectPrinter.cs diff --git a/ObjectPrinting/Solved/PrintingConfig.cs b/Solved/PrintingConfig.cs similarity index 100% rename from ObjectPrinting/Solved/PrintingConfig.cs rename to Solved/PrintingConfig.cs diff --git a/ObjectPrinting/Solved/PropertyPrintingConfig.cs b/Solved/PropertyPrintingConfig.cs similarity index 100% rename from ObjectPrinting/Solved/PropertyPrintingConfig.cs rename to Solved/PropertyPrintingConfig.cs diff --git a/ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs b/Solved/PropertyPrintingConfigExtensions.cs similarity index 100% rename from ObjectPrinting/Solved/PropertyPrintingConfigExtensions.cs rename to Solved/PropertyPrintingConfigExtensions.cs diff --git a/ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs b/Solved/Tests/ObjectPrinterAcceptanceTests.cs similarity index 100% rename from ObjectPrinting/Solved/Tests/ObjectPrinterAcceptanceTests.cs rename to Solved/Tests/ObjectPrinterAcceptanceTests.cs diff --git a/ObjectPrinting/Solved/Tests/Person.cs b/Solved/Tests/Person.cs similarity index 100% rename from ObjectPrinting/Solved/Tests/Person.cs rename to Solved/Tests/Person.cs diff --git a/fluent-api.sln.DotSettings b/fluent-api.sln.DotSettings index 135b83ec..229f449d 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" 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" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016