diff --git a/ObjectPrinting/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter.cs deleted file mode 100644 index 3c7867c32..000000000 --- a/ObjectPrinting/ObjectPrinter.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ObjectPrinting -{ - public class ObjectPrinter - { - public static PrintingConfig For() - { - return new PrintingConfig(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs new file mode 100644 index 000000000..adbf30413 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/MemberPrintingConfig.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Reflection; + +namespace ObjectPrinting.ObjectPrinter; + +public class MemberPrintingConfig( + PrintingConfig printingConfig, + MemberInfo? memberInfo = null) +{ + protected readonly PrintingConfig PrintingConfig = printingConfig; + private readonly MemberInfo? MemberInfo = memberInfo; + + public PrintingConfig Using(Func printingMethod) + { + if (MemberInfo is null) + PrintingConfig.CustomTypeSerializers[typeof(TMemberType)] = Wrapper; + else + PrintingConfig.CustomMemberSerializers[MemberInfo] = Wrapper; + + return PrintingConfig; + + string Wrapper(object value) => printingMethod((TMemberType)value); + } + + public PrintingConfig Using(CultureInfo culture) + { + PrintingConfig.CulturesForTypes[typeof(TMemberType)] = culture; + return PrintingConfig; + } + + public PrintingConfig TrimmedToLength(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + + if (MemberInfo is null) + PrintingConfig.TrimmedTypes[typeof(TMemberType)] = length; + else + PrintingConfig.TrimmedMembers[MemberInfo] = length; + + return PrintingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs b/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs new file mode 100644 index 000000000..e990ab983 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/ObjectPrinter.cs @@ -0,0 +1,9 @@ +namespace ObjectPrinting.ObjectPrinter; + +public static class ObjectPrinter +{ + public static PrintingConfig For() + { + return new PrintingConfig(); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs new file mode 100644 index 000000000..04bbc424a --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/ObjectPrintingExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace ObjectPrinting.ObjectPrinter; + +public static class ObjectPrintingExtensions +{ + 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/ObjectPrinter/PrintingConfig.cs b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs new file mode 100644 index 000000000..408120edb --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/PrintingConfig.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; + +namespace ObjectPrinting.ObjectPrinter; + +public class PrintingConfig +{ + public readonly HashSet ExcludedTypes = []; + public readonly HashSet ExcludedMember = []; + public readonly Dictionary TrimmedMembers = new(); + public readonly Dictionary CulturesForTypes = new(); + internal readonly Dictionary> CustomTypeSerializers = new(); + internal readonly Dictionary> CustomMemberSerializers = new(); + public readonly Dictionary TrimmedTypes = new(); + + public int MaxNestingLevel { get; init; } = 5; + + public PrintingConfig Excluding() + { + ExcludedTypes.Add(typeof(TMember)); + return this; + } + + public PrintingConfig Excluding(Expression> memberSelector) + { + ExcludedMember.Add(GetMember(memberSelector)); + return this; + } + + public MemberPrintingConfig Printing() + => new(this); + + public StringPrintConfig Printing(Expression> stringMemberSelector) + => new(this, GetMember(stringMemberSelector)); + + public MemberPrintingConfig Printing(Expression> memberSelector) + => new(this, GetMember(memberSelector)); + + + public string PrintToString(TOwner? obj) + => Serializer.SerializeObject(obj, this); + + + private static MemberInfo GetMember(Expression> selector) + { + if (selector.Body is MemberExpression m) + return m.Member; + throw new ArgumentException("Member selector must be a simple member access.", nameof(selector)); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/SerializationContext.cs b/ObjectPrinting/ObjectPrinter/SerializationContext.cs new file mode 100644 index 000000000..f95ad16cb --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/SerializationContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text; + +namespace ObjectPrinting.ObjectPrinter; + +internal class SerializationContext(PrintingConfig config, StringBuilder builder) +{ + public readonly PrintingConfig Config = config; + public readonly HashSet Visited = new(ReferenceEqualityComparer.Instance); + public readonly StringBuilder Builder = builder; + + public void Indent(int level) => Builder.Append(new string('\t', level)); +} + + diff --git a/ObjectPrinting/ObjectPrinter/Serializer.cs b/ObjectPrinting/ObjectPrinter/Serializer.cs new file mode 100644 index 000000000..38dc983de --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/Serializer.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ObjectPrinting.ObjectPrinter; + +internal static class Serializer +{ + private static readonly HashSet TerminalTypes = + [ + typeof(string), typeof(DateTime), typeof(TimeSpan), + typeof(Guid), typeof(decimal), + typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), + typeof(int), typeof(uint), typeof(long), typeof(ulong), + typeof(float), typeof(double), typeof(bool), typeof(char) + ]; + + internal static string SerializeObject(TOwner? obj, PrintingConfig config) + { + var sb = new StringBuilder(); + var ctx = new SerializationContext(config, sb); + Serialize(obj, 0, null, ctx); + return sb.ToString(); + } + + private static void Serialize( + object? obj, + int level, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (TryHandleNullOrExcludedOrDepth(obj, level, ctx)) + return; + + var type = obj!.GetType(); + + if (TryHandleCycle(obj, type, ctx)) + return; + + if (TryHandleCustomTypeSerializer(obj, type, parentMember, ctx)) + return; + + if (TryHandleTerminal(obj, type, parentMember, ctx)) + return; + + if (TryHandleDictionary(obj, type, level, ctx)) + return; + + if (TryHandleEnumerable(obj, type, level, ctx)) + return; + + SerializeComplexObject(obj, type, level, ctx); + } + + + private static bool TryHandleNullOrExcludedOrDepth( + object? obj, + int level, + SerializationContext ctx) + { + if (obj is null) + { + ctx.Builder.AppendLine("null"); + return true; + } + + var type = obj.GetType(); + + if (ctx.Config.ExcludedTypes.Contains(type)) + { + ctx.Builder.AppendLine("[Excluded Type]"); + return true; + } + + if (level >= ctx.Config.MaxNestingLevel) + { + ctx.Builder.AppendLine($"[{FormatTypeName(type)} превышен уровень вложенности]"); + return true; + } + + return false; + } + + private static bool TryHandleCycle( + object obj, + Type type, + SerializationContext ctx) + { + if (type.IsValueType) + return false; + + if (ctx.Visited.Add(obj)) + return false; + ctx.Builder.AppendLine($"[CyclicRef {FormatTypeName(type)}]"); + return true; + + } + + private static bool TryHandleCustomTypeSerializer( + object obj, + Type type, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (!ctx.Config.CustomTypeSerializers.TryGetValue(type, out var typeSer)) + return false; + + var text = typeSer(obj); + ctx.Builder.Append(ApplyTrimming(text, parentMember, ctx.Config)); + return true; + } + + private static bool TryHandleTerminal( + object obj, + Type type, + MemberInfo? parentMember, + SerializationContext ctx) + { + if (!TerminalTypes.Contains(type) && !type.IsEnum) + return false; + + ctx.Builder.Append(FormatScalar(obj, type, parentMember, ctx.Config)); + return true; + } + + private static bool TryHandleDictionary( + object obj, + Type type, + int level, + SerializationContext ctx) + { + if (obj is not IDictionary dict) + return false; + + ctx.Builder.AppendLine(FormatTypeName(type)); + foreach (DictionaryEntry entry in dict) + { + ctx.Indent(level + 1); + ctx.Builder.Append("Key = "); + Serialize(entry.Key, level + 1, null, ctx); + + ctx.Indent(level + 1); + ctx.Builder.Append("Value = "); + Serialize(entry.Value, level + 1, null, ctx); + } + + return true; + } + + private static bool TryHandleEnumerable( + object obj, + Type type, + int level, + SerializationContext ctx) + { + if (obj is not IEnumerable enumerable || type == typeof(string)) + return false; + + ctx.Builder.AppendLine(FormatTypeName(type)); + var i = 0; + foreach (var item in enumerable) + { + ctx.Indent(level + 1); + ctx.Builder.Append($"[{i}] = "); + Serialize(item, level + 1, null, ctx); + i++; + } + + return true; + } + + private static void SerializeComplexObject( + object obj, + Type type, + int level, + SerializationContext ctx) + { + ctx.Builder.AppendLine(FormatTypeName(type)); + + foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!prop.CanRead) continue; + + SerializeMember(prop, level, + p => p.PropertyType, + p => p.GetValue(obj), + p => p.Name, + ctx); + } + + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) + { + SerializeMember(field, level, + f => f.FieldType, + f => f.GetValue(obj), + f => f.Name, + ctx); + } + } + + + private static string FormatScalar( + object value, + Type type, + MemberInfo? parentMember, + PrintingConfig config) + { + if (config.CustomTypeSerializers.TryGetValue(type, out var del)) + { + var s = del(value); + return ApplyTrimming(s, parentMember, config); + } + + string raw; + + if (type == typeof(string)) + raw = (string)value; + else if (config.CulturesForTypes.TryGetValue(type, out var culture) && value is IFormattable fmt) + raw = fmt.ToString(null, culture); + else + raw = value.ToString() ?? string.Empty; + + return ApplyTrimming(raw, parentMember, config); + } + + private static string ApplyTrimming( + string? s, + MemberInfo? parentMember, + PrintingConfig config) + { + int? length = null; + + if (parentMember != null && + config.TrimmedMembers.TryGetValue(parentMember, out var memberLen)) + { + length = memberLen; + } + else if (parentMember is PropertyInfo pi && + config.TrimmedTypes.TryGetValue(pi.PropertyType, out var typeLen)) + { + length = typeLen; + } + else if (parentMember is FieldInfo fi && + config.TrimmedTypes.TryGetValue(fi.FieldType, out var fieldTypeLen)) + { + length = fieldTypeLen; + } + + if (length.HasValue && s != null && s.Length > length.Value) + s = s[..length.Value]; + + Debug.Assert(s != null, nameof(s) + " != null"); + return s.EndsWith(Environment.NewLine) ? s : s + Environment.NewLine; + } + + private static object? SafeGet(Func getter) + { + try + { + return getter(); + } + catch + { + return null; + } + } + + private static string FormatTypeName(Type t) + { + if (!t.IsGenericType) + return t.Name; + + var defName = t.Name; + var backtick = defName.IndexOf('`'); + defName = defName[..backtick]; + + var args = t.GetGenericArguments(); + var argNames = string.Join(", ", args.Select(FormatTypeName)); + return $"{defName}<{argNames}>"; + } + + private static void SerializeMember( + TMember member, + int level, + Func getMemberType, + Func getValue, + Func getName, + SerializationContext ctx) + where TMember : MemberInfo + { + if (ctx.Config.ExcludedMember.Contains(member)) return; + if (ctx.Config.ExcludedTypes.Contains(getMemberType(member))) return; + + ctx.Indent(level + 1); + ctx.Builder.Append(getName(member)).Append(" = "); + + if (ctx.Config.CustomMemberSerializers.TryGetValue(member, out var memberSer)) + { + var value = SafeGet(() => getValue(member)); + var text = value is null ? "null" : memberSer(value); + ctx.Builder.AppendLine(ApplyTrimming(text, member, ctx.Config).TrimEnd('\r', '\n')); + return; + } + + var memberValue = SafeGet(() => getValue(member)); + Serialize(memberValue, level + 1, member, ctx); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs new file mode 100644 index 000000000..e9458ae49 --- /dev/null +++ b/ObjectPrinting/ObjectPrinter/StringMemberPrintingConfig.cs @@ -0,0 +1,15 @@ +using System; +using System.Reflection; + +namespace ObjectPrinting.ObjectPrinter; + +public class StringPrintConfig(PrintingConfig printingConfig, MemberInfo memberInfo) + : MemberPrintingConfig(printingConfig, memberInfo) +{ + public PrintingConfig StringTrimmedToLength(int length) //по сути уже не нужен, так как дублирует поведение TrimmedToLength. + { //Просто хотелось соблюсти формальности, чтобы этот метод отображался только у string. + ArgumentOutOfRangeException.ThrowIfNegative(length); + printingConfig.TrimmedMembers[memberInfo] = length; + return PrintingConfig; + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs new file mode 100644 index 000000000..61f7a5a7d --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterAcceptanceTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using NUnit.Framework; +using ObjectPrinting.ObjectPrinter; + + +namespace ObjectPrinting.ObjectPrinterTests; + +[TestFixture] +public class ObjectPrinterAcceptanceTests +{ + [Test] + public void Demo() + { + var person = new Person + { + Id = Guid.NewGuid(), + Name = "Alex", + Age = 19, + Height = 161.14, + Scores = [100, 95, 88], + Tags = ["student", "intern", "dotnet"], + Ratings = new Dictionary + { + ["math"] = 5, + ["cs"] = 5, + ["english"] = 4 + } + }; + person.Friend = person; + + var printer = ObjectPrinter.ObjectPrinter.For() + .Excluding() + .Excluding(p => p.Age) + .Printing().Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}") + .Printing>() + .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]") + .Printing>().Using(dict => + { + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + }) + .Printing().Using(n => $"Number: {n}") + .Printing().Using(CultureInfo.InvariantCulture) + .Printing(p => p.Name).TrimmedToLength(3) + .Printing(p => p.Age).TrimmedToLength(1); + + var s1 = printer.PrintToString(person); + var s2 = person.PrintToString(); + var s3 = person.PrintToString(); + + Console.WriteLine(s1); + Console.WriteLine(s2); + Console.WriteLine(s3); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs new file mode 100644 index 000000000..13d21b50b --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/ObjectPrinterTests.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using ObjectPrinting.ObjectPrinter; + +namespace ObjectPrinting.ObjectPrinterTests; + +[TestFixture] +public class ObjectPrinterStandardSerializationTests +{ + private Person? setUpPerson; + + [OneTimeSetUp] + public void Setup() + { + setUpPerson = new Person + { + Id = Guid.Parse("f14dd761-3260-4463-a4ad-6ba14de2026c"), + Name = "Fai", + Height = 160.5, + Age = 20, + IQ = 120, + BirthDate = new DateTime(2029, 11, 16, 04, 05, 06, DateTimeKind.Utc), + Scores = [95, 88, 76], + Tags = ["brooch", "Nevada"], + Ratings = new Dictionary + { + ["quality"] = 5, + ["speed"] = 4 + }, + Friend = new Person + { + Id = Guid.Parse("cd55ace6-ac55-4f85-9434-82672b2fd9ee"), + Name = "Sigma", + Height = 180.3, + Age = 81, + Scores = [93, 55, 99, 32], + Tags = ["robots", "moon", "zero"], + Ratings = new Dictionary(), + Friend = null + } + }; + } + + [Test] + public void PrintToString_ShouldIncludeBasicProperties_WhenPersonIsValid() + { + var actual = setUpPerson.PrintToString(); + actual.Should().NotBeNullOrEmpty(); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = {setUpPerson.Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Height)} = {setUpPerson.Height}"); + actual.Should().Contain($"{nameof(setUpPerson.Age)} = {setUpPerson.Age}"); + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = {setUpPerson.IQ}"); + actual.Should().Contain($"{nameof(setUpPerson.BirthDate)} = {setUpPerson.BirthDate}"); + actual.Should().Contain($"{nameof(setUpPerson.Scores)} = {setUpPerson.Scores.GetType().Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Tags)} = List"); + actual.Should().Contain($"{nameof(setUpPerson.Ratings)} = Dictionary"); + actual.Should().Contain($"{nameof(setUpPerson.Friend)} = {setUpPerson.Friend!.GetType().Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Friend.Friend)} = {null}"); + } + + + [Test] + public void PrintToString_ShouldApplyMemberExclusion_WhenMemberExcludedInConfig() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding(p => p.Name)); + + actual.Should().NotContain($"{nameof(setUpPerson.Name)} = {setUpPerson!.Name}"); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); + } + + [Test] + public void PrintToString_ShouldApplyTypeExclusion_WhenTypeExcludedInConfig() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding()); + + actual.Should().NotContain($"{nameof(setUpPerson.Age)} = {setUpPerson!.Age}"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = {setUpPerson!.IQ}"); + actual.Should().Contain($"{nameof(setUpPerson.Id)} = {setUpPerson!.Id}"); + } + + [Test] + public void PrintToString_ShouldUseCustomFormatter_WhenTypeFormatterIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(n => $"|{n}|")); + + actual.Should().Contain("Age = |20|"); + actual.Should().NotContain("Age = 20"); + actual.Should().Contain("IQ = |120|"); + actual.Should().NotContain("IQ = 120"); + } + + [Test] + public void PrintToString_ShouldUseCustomFormatter_WhenMemberFormatterIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.Age).Using(n => $"Age: {n}")); + + actual.Should().Contain("Age: 20"); + actual.Should().NotContain("Age = 20"); + actual.Should().Contain("IQ = 120"); + actual.Should().NotContain("IQ: 120"); + } + + [Test] + public void PrintToString_ShouldUseInvariantCulture_WhenDoubleCultureIsSetToInvariant() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(CultureInfo.InvariantCulture)); + + actual.Should().Contain($"{nameof(setUpPerson.Height)} = 160.5"); + actual.Should().NotContain($"{nameof(setUpPerson.BirthDate)} = 11/16/2029 04:05:06"); + } + + [Test] + public void PrintToString_ShouldUseInvariantCulture_WhenDateTimeCultureIsSetToInvariant() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().Using(CultureInfo.InvariantCulture)); + + actual.Should().Contain($"{nameof(setUpPerson.BirthDate)} = 11/16/2029 04:05:06"); + actual.Should().NotContain($"{nameof(setUpPerson.Height)} = 160.5"); + } + + + [Test] + public void PrintToString_ShouldApplyMemberSerializer_WhenNameCustomSerializerIsSet() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p!.Name).Using(p => p.ToUpper())); + + actual.Should().Contain($"{nameof(setUpPerson.Name)} = FAI"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenStringMemberTrimmedToLengthSpecified() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.Name).StringTrimmedToLength(2)); + + actual.Should().Contain($"{nameof(setUpPerson.Name)} = Fa"); + actual.Should().NotContain($"{nameof(setUpPerson.Name)} = Fai"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenIntMemberTrimmedToLengthSpecified() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing(p => p.IQ).TrimmedToLength(2)); + + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = 12"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = 120"); + } + + [Test] + public void PrintToString_ShouldTrimStringProperty_WhenIntTypeTrimmedToLengthSpecified() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing().TrimmedToLength(1)); + + actual.Should().Contain($"{nameof(setUpPerson.IQ)} = 1"); + actual.Should().NotContain($"{nameof(setUpPerson.IQ)} = 120"); + actual.Should().Contain($"{nameof(setUpPerson.Age)} = 2"); + actual.Should().NotContain($"{nameof(setUpPerson.Age)} = 20"); + } + + [Test] + public void PrintToString_ShouldApplyMultipleCustomizations_WhenCombinedExclusionsAndSerializersUsed() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Excluding() + .Excluding(p => p.Tags) + .Printing().Using(n => $"Number: {n}") + .Printing().Using(CultureInfo.InvariantCulture) + .Printing(p => p.Name).TrimmedToLength(2) + .Printing(p => p.Name).Using(p => p.ToUpper())); + + actual.Should().NotContain($"{nameof(setUpPerson.Id)}"); + actual.Should().NotContain($"{nameof(setUpPerson.Tags)}"); + actual.Should().Contain("Number: 20"); + actual.Should().NotContain("Age = 20"); + actual.Should().Contain($"{nameof(setUpPerson.Height)} = 160.5"); + actual.Should().NotContain($"{nameof(setUpPerson.Height)} = 160,5"); + actual.Should().Contain($"{nameof(setUpPerson.Name)} = FA"); + } + + [Test] + public void PrintToString_ShouldRenderIntArrayWithIndexedItems_WhenPersonHasArray() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain($"{nameof(setUpPerson.Scores)} = {setUpPerson!.Scores.GetType().Name}"); + actual.Should().Contain($"[0] = {setUpPerson.Scores[0]}"); + actual.Should().Contain($"[1] = {setUpPerson.Scores[1]}"); + actual.Should().Contain($"[2] = {setUpPerson.Scores[2]}"); + } + + [Test] + public void PrintToString_ShouldRenderGenericListWithIndexedItems_WhenPersonHasList() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain($"{nameof(setUpPerson.Tags)} = List"); + actual.Should().Contain($"[0] = {setUpPerson.Tags[0]}"); + actual.Should().Contain($"[1] = {setUpPerson.Tags[1]}"); + } + + [Test] + public void PrintToString_ShouldRenderDictionaryWithKeyValuePairs_WhenPersonHasDictionary() + { + var actual = setUpPerson.PrintToString(); + + actual.Should().Contain($"{nameof(setUpPerson.Ratings)} = Dictionary"); + actual.Should().Contain($"Key = {setUpPerson!.Ratings.Keys.First()}"); + actual.Should().Contain($"Value = {setUpPerson.Ratings.Values.First()}"); + actual.Should().Contain($"Key = {setUpPerson.Ratings.Keys.Last()}"); + actual.Should().Contain($"Value = {setUpPerson.Ratings.Values.Last()}"); + } + + [Test] + public void PrintToString_ShouldRenderMultidimensionalArrayRank_WhenArrayIsTwoDimensional() + { + var matrix = new[,] + { + { 1, 1, 1 }, + { 1, 1, 1 } + }; + + var actual = matrix.PrintToString(); + + actual.Should().Contain("Int32[,]"); + } + + [Test] + public void PrintToString_ShouldRenderJaggedArraySuffixes_WhenArrayIsArrayOfArrays() + { + var jagged = new int[][] + { + [1, 2, 3], + [4, 5, 6] + }; + + var actual = jagged.PrintToString(); + + actual.Should().Contain("Int32[][]"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenIntArraySerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing() + .Using(arr => $"int[{arr.Length}] {{ {string.Join(", ", arr)} }}")); + + actual.Should().Contain("Scores = int[3] { 95, 88, 76 }"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenStringListSerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing>() + .Using(list => $"List (Count = {list.Count}) [ {string.Join(", ", list)} ]")); + + actual.Should().Contain("Tags = List (Count = 2) [ brooch, Nevada ]"); + } + + [Test] + public void PrintToString_ShouldUseCustomSerializer_WhenDictionarySerializerIsConfigured() + { + var actual = setUpPerson.PrintToString(cfg => cfg + .Printing>() + .Using(dict => + { + var pairs = dict.Select(kv => $"{kv.Key}: {kv.Value}"); + return $"Dictionary (Count = {dict.Count}) {{ {string.Join(", ", pairs)} }}"; + })); + + actual.Should().Contain("Ratings = Dictionary (Count = 2) { quality: 5, speed: 4 }"); + } + + [Test] + public void PrintToString_ShouldEmitCycleMarker_WhenObjectGraphHasCyclicReference() + { + var person = new Person(); + person.Friend = person; + + var act = () => person.PrintToString(); + var actual = person.PrintToString(); + + act.Should().NotThrow(); + actual.Should().Contain("CyclicRef"); + } + + [Test] + public void PrintToString_ShouldNotShareVisitedState_WhenDifferentRootsShareSameFriend() + { + var sharedFriend = new Person { Name = "Shared", Age = 42 }; + + var person1 = new Person { Name = "P1", Friend = sharedFriend }; + + var person2 = new Person { Name = "P2", Friend = sharedFriend }; + + var actual1 = person1.PrintToString(); + var actual2 = person2.PrintToString(); + + + actual1.Should().Contain("Friend = Person"); + actual1.Should().NotContain("CyclicRef"); + + actual2.Should().Contain("Friend = Person"); + actual2.Should().NotContain("CyclicRef"); + } + + + [Test] + public void PrintToString_ShouldReportNestingLevelExceeded_WhenFriendChainDepthBeyondMax() + { + var person = new Person(); + person.Friend = new Person(); + person.Friend.Friend = new Person(); + + var printer = new PrintingConfig + { + MaxNestingLevel = 2 + }; + + var actual = printer.PrintToString(person); + + actual.Should().Contain("превышен уровень вложенности"); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinterTests/Person.cs b/ObjectPrinting/ObjectPrinterTests/Person.cs new file mode 100644 index 000000000..4fe158a92 --- /dev/null +++ b/ObjectPrinting/ObjectPrinterTests/Person.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace ObjectPrinting.ObjectPrinterTests +{ + public class Person + { + public Guid Id { get; set; } + public string Name { get; set; } + public double Height { get; set; } + public int Age { get; set; } + + public int IQ { get; set; } + + public DateTime BirthDate {get; set;} + public int[] Scores { get; set; } + + public List Tags { get; set; } + + public Dictionary Ratings { get; set; } + + public Person? Friend { get; set; } + } +} \ 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/ObjectPrinting.csproj.DotSettings b/ObjectPrinting/ObjectPrinting.csproj.DotSettings new file mode 100644 index 000000000..28a74f772 --- /dev/null +++ b/ObjectPrinting/ObjectPrinting.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs deleted file mode 100644 index a9e082117..000000000 --- a/ObjectPrinting/PrintingConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Text; - -namespace ObjectPrinting -{ - public class PrintingConfig - { - public string PrintToString(TOwner obj) - { - return PrintToString(obj, 0); - } - - private string PrintToString(object obj, int nestingLevel) - { - //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; - - var finalTypes = new[] - { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; - - var identation = new string('\t', nestingLevel + 1); - var sb = new StringBuilder(); - var type = obj.GetType(); - sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) - { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/ObjectPrinting/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/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