diff --git a/ObjectPrinting.Tests/BasePerson.cs b/ObjectPrinting.Tests/BasePerson.cs new file mode 100644 index 00000000..cad59fa1 --- /dev/null +++ b/ObjectPrinting.Tests/BasePerson.cs @@ -0,0 +1,22 @@ +namespace ObjectPrinting.Tests; + +public class BasePerson +{ + public Guid Id { get; set; } + public string Name { get; set; } + public double Height { get; set; } + public int Age { get; set; } + public int[] Array { get; set; } + + public static BasePerson Get() + { + return new BasePerson + { + Id = new Guid(), + Age = 20, + Height = 183.4, + Name = "Peter", + Array = new []{2, 3, 4}, + }; + } +} \ No newline at end of file diff --git a/ObjectPrinting.Tests/ExtendedPerson.cs b/ObjectPrinting.Tests/ExtendedPerson.cs new file mode 100644 index 00000000..4f401b2e --- /dev/null +++ b/ObjectPrinting.Tests/ExtendedPerson.cs @@ -0,0 +1,55 @@ +namespace ObjectPrinting.Tests; + +public class ExtendedPerson +{ + public Guid Id { get; set; } + public int Age { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public List Children { get; set; } + public char[] Array { get; set; } + public Dictionary Dict { get; set; } + + public static ExtendedPerson Get() + { + var person1 = new ExtendedPerson() + { + Id = new Guid(), + Age = 18, + FirstName = "Peter", + LastName = "Gromov", + Children = new List(), + Array = null, + Dict = null, + }; + + var person2 = new ExtendedPerson() + { + Id = new Guid(), + Age = 20, + FirstName = "Peter", + LastName = "Gromov", + Children = new List(), + Array = null, + Dict = null, + }; + person1.Children.Add(person2); + person2.Children.Add(person1); + + + return new ExtendedPerson + { + Id = new Guid(), + Age = 22, + FirstName = "Peter", + LastName = "Gromov", + Children = new List(new[] { person1, person2 }), + Array = "fds6".ToCharArray(), + Dict = new Dictionary() + { + [3] = person1, + [2] = null, + } + }; + } +} \ No newline at end of file diff --git a/ObjectPrinting.Tests/ObjectPrinting.Tests.csproj b/ObjectPrinting.Tests/ObjectPrinting.Tests.csproj new file mode 100644 index 00000000..8f9d1205 --- /dev/null +++ b/ObjectPrinting.Tests/ObjectPrinting.Tests.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/ObjectPrinting.Tests/PrintingConfigTests.cs b/ObjectPrinting.Tests/PrintingConfigTests.cs new file mode 100644 index 00000000..d9405dc0 --- /dev/null +++ b/ObjectPrinting.Tests/PrintingConfigTests.cs @@ -0,0 +1,170 @@ +using System.Globalization; +using FluentAssertions; +using ObjectPrinting.Configs; +using ObjectPrinting.Extensions; + +namespace ObjectPrinting.Tests; + +[TestFixture] +[TestOf(typeof(ObjectPrinter))] +public class PrintingConfigTests +{ + private readonly BasePerson basePerson = BasePerson.Get(); + private PrintingConfig printingConfig = ObjectPrinter.For(); + + [SetUp] + public void SetUp() + { + printingConfig = ObjectPrinter.For(); + } + + [Test] + public void Should_NotThrow_WhenInputNull() + { + var config = ObjectPrinter.For(); + Action action = () => config.PrintToString(null); + + action.Should().NotThrow(); + } + + [Test] + public void API_Example() + { + printingConfig + //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); + + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + basePerson.PrintToString(); + //8. ...с конфигурированием + basePerson.PrintToString(s => s.Excluding(p => p.Age)); + } + + [Test] + public void Should_UseCultureInfo() + { + var culture = CultureInfo.GetCultureInfo("ru-RU"); + printingConfig + .Printing() + .Using(culture); + + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().Contain(basePerson.Height.ToString(culture)); + } + + [Test] + public void Should_TrimString() + { + printingConfig + .Printing() + .TrimmedToLength(2); + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().Contain("Name = Pe\r\n"); + } + + [Test] + public void Should_IgnoreExcludedProperty() + { + printingConfig.Excluding(p => p.Array); + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().NotContain(nameof(basePerson.Array)); + } + + + [Test] + [TestOf(nameof(PrintingConfig.Excluding))] + public void Should_IgnoreExcludedType() + { + printingConfig.Excluding(); + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().NotContain(nameof(basePerson.Id)); + } + + [Test] + [TestOf(nameof(PrintingConfig.Excluding))] + public void Should_NotThrow_WhenContainsCyclicLinks() + { + var mock = ExtendedPerson.Get(); + var config = ObjectPrinter.For(); + Action action = () => config.PrintToString(mock); + + action.Should().NotThrow(); + } + + [Test] + [TestOf(nameof(PrintingConfig.Excluding))] + public void Should_SupportCustomPropertySerializer() + { + printingConfig + .Printing(p => p.Age) + .Using(v => "123123"); + + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().Contain("Age = 123123"); + } + + [Test] + [TestOf(nameof(PrintingConfig.Excluding))] + public void Should_SupportCustomTypeSerializer() + { + printingConfig + .Printing() + .Using(v => "123123"); + + var actual = printingConfig.PrintToString(basePerson); + + actual.Should().Contain("Age = 123123"); + } + + [Test] + public void Should_SupportArray() + { + var array = new object[] { 1, 2, 3 }; + + var actual = array.PrintToString(); + + var expected = "\r\n[\r\n\t1\r\n\t2\r\n\t3\r\n]"; + actual.Should().Contain(expected); + } + + [Test] + public void Should_SupportList() + { + var list = new List { 1, 2, 3 }; + + var actual = list.PrintToString(); + + var expected = "\r\n[\r\n\t1\r\n\t2\r\n\t3\r\n]"; + actual.Should().Contain(expected); + } + + [Test] + public void Should_SupportDictionary() + { + var dict = new Dictionary + { + [1] = "123", + [3] = "345", + }; + + var actual = dict.PrintToString(); + + var expected = "{\r\n\t1: 123\r\n\t3: 345\r\n}"; + + actual.Should().Contain(expected); + } +} \ No newline at end of file diff --git a/ObjectPrinting.Tests/Usings.cs b/ObjectPrinting.Tests/Usings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/ObjectPrinting.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/ObjectPrinting/Configs/IPropertyPrintingConfig.cs b/ObjectPrinting/Configs/IPropertyPrintingConfig.cs new file mode 100644 index 00000000..07389c93 --- /dev/null +++ b/ObjectPrinting/Configs/IPropertyPrintingConfig.cs @@ -0,0 +1,11 @@ +using System; + +namespace ObjectPrinting.Configs +{ + public interface IPropertyPrintingConfig + { + public PrintingConfig PrintingConfig { get; } + + public Func Serializer { get; } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/ITypePrintingConfig.cs b/ObjectPrinting/Configs/ITypePrintingConfig.cs new file mode 100644 index 00000000..55214f11 --- /dev/null +++ b/ObjectPrinting/Configs/ITypePrintingConfig.cs @@ -0,0 +1,14 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting.Configs +{ + public interface ITypePrintingConfig + { + public PrintingConfig PrintingConfig { get; } + + public CultureInfo CultureInfo { get;} + + public Func Serializer { get; } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/PropertyPrintingConfig.cs b/ObjectPrinting/Configs/PropertyPrintingConfig.cs new file mode 100644 index 00000000..3af85bf6 --- /dev/null +++ b/ObjectPrinting/Configs/PropertyPrintingConfig.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting.Configs +{ + public class PropertyPrintingConfig : IPropertyPrintingConfig + { + private PropertyInfo property; + // private Func serializer; + + public PrintingConfig PrintingConfig { get; } + public Func Serializer { get; private set; } + // public Func IPropertyPrintingConfig.Serializer { get; } + + public PropertyPrintingConfig(PrintingConfig printingConfig, PropertyInfo property) + { + PrintingConfig = printingConfig; + this.property = property; + } + + public PrintingConfig Using(Func serializer) + { + Serializer = obj => + { + if (obj is TPropType value) + return serializer(value); + + throw new ArgumentException(); + }; + + return PrintingConfig; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Configs/TypePrintingConfig.cs b/ObjectPrinting/Configs/TypePrintingConfig.cs new file mode 100644 index 00000000..44263a9c --- /dev/null +++ b/ObjectPrinting/Configs/TypePrintingConfig.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; + +namespace ObjectPrinting.Configs +{ + public class TypePrintingConfig : ITypePrintingConfig + { + public PrintingConfig PrintingConfig { get; } + public CultureInfo CultureInfo { get; private set; } + public Func Serializer { get; private set; } + + public TypePrintingConfig(PrintingConfig printingConfig) + { + PrintingConfig = printingConfig; + } + + public PrintingConfig Using(CultureInfo cultureInfo) + { + CultureInfo = cultureInfo; + + return PrintingConfig; + } + + public PrintingConfig Using(Func serializer) + { + Serializer = obj => + { + if (obj is TType value) + return serializer(value); + + throw new ArgumentException(); + }; + + return PrintingConfig; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/ObjectExtensions.cs b/ObjectPrinting/Extensions/ObjectExtensions.cs new file mode 100644 index 00000000..c1975e53 --- /dev/null +++ b/ObjectPrinting/Extensions/ObjectExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace ObjectPrinting.Extensions +{ + public static class ObjectExtensions + { + public static string PrintToString(this TOwner obj) => PrintToString(obj, c => c); + + public static string PrintToString(this TOwner obj, + Func, PrintingConfig> config) + { + return config(new PrintingConfig()) + .PrintToString(obj); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/PropertyInfoExtensions.cs b/ObjectPrinting/Extensions/PropertyInfoExtensions.cs new file mode 100644 index 00000000..ab2a2703 --- /dev/null +++ b/ObjectPrinting/Extensions/PropertyInfoExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ObjectPrinting.Extensions +{ + public static class PropertyInfoExtensions + { + public static IEnumerable IgnoreTypes(this IEnumerable properties, IEnumerable ignoredTypes) + { + return properties + .Where(p => !ignoredTypes.Contains(p.PropertyType)); + } + + public static IEnumerable IgnoreProperties(this IEnumerable properties, IEnumerable ignoredProperties) + { + return properties + .Except(ignoredProperties); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/Extensions/PropertyPrintingConfigExtensions.cs new file mode 100644 index 00000000..8d61edd2 --- /dev/null +++ b/ObjectPrinting/Extensions/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,12 @@ +using ObjectPrinting.Configs; + +namespace ObjectPrinting.Extensions +{ + public static class PropertyPrintingConfigExtensions + { + public static PrintingConfig TrimmedToLength(this PropertyPrintingConfig propertyConfig, int length) + { + return propertyConfig.Using(s => s.Truncate(length)); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/StringExtensions.cs b/ObjectPrinting/Extensions/StringExtensions.cs new file mode 100644 index 00000000..8db41b94 --- /dev/null +++ b/ObjectPrinting/Extensions/StringExtensions.cs @@ -0,0 +1,11 @@ +namespace ObjectPrinting.Extensions +{ + public static class StringExtensions + { + public static string Truncate(this string value, int maxLength) + { + if (string.IsNullOrEmpty(value)) return value; + return value.Length <= maxLength ? value : value[..maxLength]; + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/Extensions/TypePrintingConfigExtensions.cs b/ObjectPrinting/Extensions/TypePrintingConfigExtensions.cs new file mode 100644 index 00000000..552abc1e --- /dev/null +++ b/ObjectPrinting/Extensions/TypePrintingConfigExtensions.cs @@ -0,0 +1,12 @@ +using ObjectPrinting.Configs; + +namespace ObjectPrinting.Extensions +{ + public static class TypePrintingConfigExtensions + { + public static PrintingConfig TrimmedToLength(this TypePrintingConfig typeConfig, int length) + { + return typeConfig.Using(s => s.Truncate(length)); + } + } +} \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e08211..fe564945 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,194 @@ using System; -using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using System.Text; +using ObjectPrinting.Configs; +using ObjectPrinting.Extensions; +using static ObjectPrinting.PrintingHelper; namespace ObjectPrinting { public class PrintingConfig { - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } + private readonly HashSet ignoredTypes = new HashSet(); + private readonly HashSet ignoredProperties = new HashSet(); + private readonly Dictionary visited = new Dictionary(); + + private readonly Dictionary> typeConfigs = + new Dictionary>(); + + private readonly Dictionary> propertyConfigs = + new Dictionary>(); + + public string PrintToString(TOwner obj) => PrintToString(obj, 0); private string PrintToString(object obj, int nestingLevel) { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + if (obj is null) + return NullString; - var finalTypes = new[] + var type = obj.GetType(); + if (FinalTypes.Contains(type)) { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; + return ApplyCulture(obj, type) + 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 indentation = new string('\t', nestingLevel + 1); + if (visited.TryGetValue(obj, out var level) && nestingLevel - level > MaxCyclicDepth) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + sb.Append($"{indentation}\"The limit of cyclic references\"{NewLine}"); + return sb.ToString(); } + + if (type.IsClass) + visited.TryAdd(obj, nestingLevel); + + if (obj is IEnumerable enumerable) + { + SerializeEnumerable(sb, enumerable, nestingLevel); + return sb.ToString(); + } + + var properties = type.GetProperties() + .IgnoreProperties(ignoredProperties) + .IgnoreTypes(ignoredTypes); + foreach (var property in properties) + { + var propertyValue = property.GetValue(obj); + if (TrySerializeProperty(property, sb, indentation, propertyValue)) + continue; + + if (TrySerializeType(property, sb, indentation, propertyValue)) + continue; + + sb.Append( + $"{indentation}{property.Name} = {PrintToString(propertyValue, nestingLevel + 1)}"); + } + return sb.ToString(); } + + private bool TrySerializeProperty(PropertyInfo property, StringBuilder sb, string indentation, + object propertyValue) + { + if (!propertyConfigs.TryGetValue(property, out var config) || config.Serializer is null) + return false; + + sb.Append( + $"{indentation}{property.Name} = {config.Serializer(propertyValue)}{NewLine}"); + + return true; + } + + private bool TrySerializeType(PropertyInfo property, StringBuilder sb, string indentation, + object propertyValue) + { + if (!typeConfigs.TryGetValue(property.PropertyType, out var config) || config.Serializer is null) + return false; + + sb.Append( + $"{indentation}{property.Name} = {config.Serializer(propertyValue)}{NewLine}"); + + return true; + } + + private string ApplyCulture(object obj, Type objType) + { + if (!(obj is IConvertible convertible)) + return obj.ToString(); + + if (!typeConfigs.TryGetValue(objType, out var config) || config.CultureInfo is null) + return obj.ToString(); + + return convertible.ToString(config.CultureInfo); + } + + public PrintingConfig Excluding() + { + ignoredTypes.Add(typeof(TProperty)); + + return this; + } + + public PropertyPrintingConfig Printing( + Expression> propertySelector) + { + if (!(propertySelector.Body is MemberExpression expr)) + throw new ArgumentException(nameof(propertySelector)); + + if (!(expr.Member is PropertyInfo property)) + throw new ArgumentException(nameof(propertySelector)); + + var propertyConfig = new PropertyPrintingConfig(this, property); + propertyConfigs[property] = propertyConfig; + + return propertyConfig; + } + + public TypePrintingConfig Printing() + { + var type = typeof(TPropType); + var typeConfig = new TypePrintingConfig(this); + typeConfigs[type] = typeConfig; + + return typeConfig; + } + + public PrintingConfig Excluding(Expression> propertySelector) + { + if (!(propertySelector.Body is MemberExpression expr)) + throw new ArgumentException(nameof(propertySelector)); + + if (!(expr.Member is PropertyInfo property)) + throw new ArgumentException(nameof(propertySelector)); + + ignoredProperties.Add(property); + + return this; + } + + private void SerializeEnumerable(StringBuilder sb, IEnumerable enumerable, int nestingLevel) + { + switch (enumerable) + { + case IDictionary dict: + SerializeDictionary(sb, dict, nestingLevel); + return; + case null: + sb.Append(NullString); + return; + } + + var indentation = new string('\t', nestingLevel + 1); + sb.Append($"{indentation[..^1]}[{NewLine}"); + foreach (var obj2 in enumerable) + { + sb.Append(indentation + PrintToString(obj2, nestingLevel + 1)); + } + + sb.AppendLine($"{indentation[..^1]}]"); + } + + private void SerializeDictionary(StringBuilder sb, IDictionary dict, int nestingLevel) + { + if (dict is null) + { + sb.Append(NullString); + return; + } + + var indentation = new string('\t', nestingLevel + 1); + sb.Append($"{indentation[..^1]}{{{NewLine}"); + foreach (var key in dict.Keys) + { + sb.Append($"{indentation}{key}: {PrintToString(dict[key], nestingLevel + 2)}"); + } + + sb.AppendLine($"{indentation[..^1]}}}"); + } } } \ No newline at end of file diff --git a/ObjectPrinting/PrintingHelper.cs b/ObjectPrinting/PrintingHelper.cs new file mode 100644 index 00000000..4fb70270 --- /dev/null +++ b/ObjectPrinting/PrintingHelper.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting +{ + public static class PrintingHelper + { + public const int MaxCyclicDepth = 2; + + public static readonly string NewLine = Environment.NewLine; + public static readonly string NullString = $"null{NewLine}"; + + public static readonly HashSet FinalTypes = new HashSet(new[] + { + typeof(byte), typeof(short), typeof(int), typeof(long), typeof(double), typeof(float), typeof(string), + typeof(DateTime), typeof(TimeSpan), typeof(char), typeof(bool), + }); + } +} \ No newline at end of file diff --git a/fluent-api.sln b/fluent-api.sln index 69c8db9e..144819ac 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}") = "ObjectPrinting.Tests", "ObjectPrinting.Tests\ObjectPrinting.Tests.csproj", "{610830B2-D921-4B70-AC44-73EB85D9F2A3}" +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 + {610830B2-D921-4B70-AC44-73EB85D9F2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {610830B2-D921-4B70-AC44-73EB85D9F2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {610830B2-D921-4B70-AC44-73EB85D9F2A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {610830B2-D921-4B70-AC44-73EB85D9F2A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE