diff --git a/ObjectPrinting/ObjectPrinterExtensions.cs b/ObjectPrinting/ObjectPrinterExtensions.cs new file mode 100644 index 000000000..6461e7eae --- /dev/null +++ b/ObjectPrinting/ObjectPrinterExtensions.cs @@ -0,0 +1,17 @@ +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, Func, PrintingConfig> configBuilder) + { + var config = configBuilder(ObjectPrinter.For()); + return config.PrintToString(obj); + } +} \ No newline at end of file diff --git a/ObjectPrinting/ObjectPrinting.csproj b/ObjectPrinting/ObjectPrinting.csproj index c5db392ff..8cf1eff64 100644 --- a/ObjectPrinting/ObjectPrinting.csproj +++ b/ObjectPrinting/ObjectPrinting.csproj @@ -5,8 +5,9 @@ + - + diff --git a/ObjectPrinting/PrintingConfig.cs b/ObjectPrinting/PrintingConfig.cs index a9e082117..e9e09452d 100644 --- a/ObjectPrinting/PrintingConfig.cs +++ b/ObjectPrinting/PrintingConfig.cs @@ -1,41 +1,382 @@ 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 { + public delegate string SerializerDelegate(object? obj, int identationLevel, int depthLevel, List pathSegments); + private readonly IReadOnlySet excludedTypes; + private readonly IReadOnlyDictionary typeSerializers; + private readonly IReadOnlyDictionary customCultureInfos; + private readonly IReadOnlyDictionary propertySerializers; + private readonly IReadOnlyDictionary maxStringLength; + private readonly IReadOnlySet excludedProperties; + private const int MaxDeepnessLevel = 3; + private const char Tabchar = '\t'; + private const char ResetCaretChar = '\r'; + private const char NewLineChar = '\n'; + private static readonly string EnvironmentNewLine = Environment.NewLine; + private const string EqualityWithSpaces = " = "; + private readonly string deepnessExceededString = $"Deepness exceeded!{EnvironmentNewLine}"; + + private static HashSet _finalTypes = + [ + typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(double), typeof(string), typeof(bool), + typeof(decimal), typeof(char), typeof(string), typeof(Guid), + typeof(DateTime), typeof(TimeSpan) + ]; + + public PrintingConfig() : this( + new HashSet(), + new Dictionary(), + new Dictionary(), + new Dictionary(), + new Dictionary(), + new HashSet()) + {} + + private PrintingConfig( + IReadOnlySet excludedTypes, + IReadOnlyDictionary typeSerializers, + IReadOnlyDictionary customCultureInfos, + IReadOnlyDictionary propertySerializers, + IReadOnlyDictionary maxStringLength, + IReadOnlySet excludedProperties) + { + this.excludedTypes = excludedTypes; + this.typeSerializers = typeSerializers; + this.customCultureInfos = customCultureInfos; + this.propertySerializers = propertySerializers; + this.maxStringLength = maxStringLength; + this.excludedProperties = excludedProperties; + } + + public PrintingConfig ExcludePropertyOfType() + { + if (excludedTypes.Contains(typeof(T))) + return this; + var tempSet = new HashSet(excludedTypes) { typeof(T) }; + return new PrintingConfig(tempSet, + typeSerializers, + customCultureInfos, + propertySerializers, + maxStringLength, + excludedProperties); + } + + public PrintingConfig WithTypeSerializationStyle(SerializerDelegate serializer) + { + var tempDict = new Dictionary(typeSerializers) + { + [typeof(T)] = serializer + }; + return new PrintingConfig(excludedTypes, + tempDict, + customCultureInfos, + propertySerializers, + maxStringLength, + excludedProperties); + } + + public PrintingConfig SetTypeCulture(CultureInfo cultureInfo) + where T : IFormattable + { + var tempDict = new Dictionary(customCultureInfos) + { + [typeof(T)] = cultureInfo + }; + return new PrintingConfig(excludedTypes, + typeSerializers, + tempDict, + propertySerializers, + maxStringLength, + excludedProperties); + } + + public PrintingConfig WithPropertySerialization(PropertyPath propertyName, SerializerDelegate serializer) + { + var tempDict = new Dictionary(propertySerializers) + { + [propertyName.ToString()] = serializer + }; + return new PrintingConfig(excludedTypes, + typeSerializers, + customCultureInfos, + tempDict, + maxStringLength, + excludedProperties); + } + + public PrintingConfig TrimStringToLength(Expression> propertyExpr, int length) + { + if (propertyExpr.Body is not MemberExpression member) + throw new ArgumentException("Expression must select a property"); + var tempDict = new Dictionary(maxStringLength) + { + [PropertyPath.ConvertFromSelector(propertyExpr).ToString()] = length + }; + return new PrintingConfig(excludedTypes, + typeSerializers, + customCultureInfos, + propertySerializers, + tempDict, + excludedProperties); + } + + public PrintingConfig Exclude(PropertyPath propertyName) + { + if (excludedProperties.Contains(propertyName.ToString())) + return this; + var tempSet = new HashSet(excludedProperties) { propertyName.ToString() }; + return new PrintingConfig(excludedTypes, + typeSerializers, + customCultureInfos, + propertySerializers, + maxStringLength, + tempSet); + } + + public PropertyPrintingConfig GetPropertyByName( + Expression> targetProperty) + { + return new PropertyPrintingConfig(this, targetProperty); + } + public string PrintToString(TOwner obj) { return PrintToString(obj, 0); } - private string PrintToString(object obj, int nestingLevel) + private string PrintToString(object? obj, int nestingLevel) { //TODO apply configurations - if (obj == null) - return "null" + Environment.NewLine; + if (obj is null) + return "null" + EnvironmentNewLine; + + var type = obj.GetType(); + var path = new List{type.Name}; + return GetSerializerForType(type).Invoke(obj, nestingLevel, 0, path); + } + + private string Serialize(object? obj, int nestingLevel, int deepnessLevel, List pathSegments) + { + if (obj is null) + return "null" + EnvironmentNewLine; + var type = obj.GetType(); + if (TrySerializeAsBaseFields(obj, type, out var serialized) && serialized is not null) + return serialized; + + var sb = new StringBuilder(); + sb.Append(SerializeComplexField(type, deepnessLevel, obj, nestingLevel, pathSegments)); + return sb.ToString(); + } - var finalTypes = new[] + private bool TrySerializeAsBaseFields(object obj, Type type, out string? result) + { + result = null; + if (customCultureInfos.TryGetValue(type, out var info)) { - typeof(int), typeof(double), typeof(float), typeof(string), - typeof(DateTime), typeof(TimeSpan) - }; - if (finalTypes.Contains(obj.GetType())) - return obj + Environment.NewLine; + result = (obj as IFormattable)?.ToString(null, info) + EnvironmentNewLine; + return true; + } + if (_finalTypes.Contains(type)) + { + result = obj + EnvironmentNewLine; + return true; + } + return false; + } - var identation = new string('\t', nestingLevel + 1); + private string SerializeComplexField(Type type, int deepnessLevel, object obj, int nestingLevel, List pathSegments) + { var sb = new StringBuilder(); - var type = obj.GetType(); sb.AppendLine(type.Name); - foreach (var propertyInfo in type.GetProperties()) + if (obj is IEnumerable enumerable && type != typeof(string)) + return SerializeCollection(enumerable, nestingLevel, deepnessLevel, pathSegments); + + var fullFieldName = new PropertyPath(pathSegments); + var objectInfo = type.GetMembers(BindingFlags.Instance + | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.MemberType is MemberTypes.Field or MemberTypes.Property) + .Where(x => !IsBackingField(x) && !IsShadowingProperty(x, type)) + .Where(x => !excludedTypes.Contains(ToMemberType(x))); + foreach (var propertyInfo in objectInfo) { - sb.Append(identation + propertyInfo.Name + " = " + - PrintToString(propertyInfo.GetValue(obj), - nestingLevel + 1)); + pathSegments.Add(propertyInfo.Name); + fullFieldName = new PropertyPath(pathSegments); + if (excludedProperties.Contains(fullFieldName.ToString())) + { + pathSegments.RemoveAt(pathSegments.Count - 1); + continue; + } + if (IsDeepnessOverflow(deepnessLevel)) + return deepnessExceededString; + sb.Append(SerializePropertyOrField(propertyInfo, obj, nestingLevel, deepnessLevel, pathSegments)); + pathSegments.RemoveAt(pathSegments.Count - 1); } return sb.ToString(); } + + private Type ToMemberType(MemberInfo member) + { + if (member is PropertyInfo propertyInfo) + return propertyInfo.PropertyType; + if (member is FieldInfo fieldInfo) + return fieldInfo.FieldType; + throw new ArgumentException($"{member.GetType().Name} is not a property or field"); + } + + private object? GetMemberValue(MemberInfo member, object? obj) + { + if (member is PropertyInfo propertyInfo) + return propertyInfo.GetValue(obj); + if (member is FieldInfo fieldInfo) + return fieldInfo.GetValue(obj); + throw new ArgumentException($"{member.GetType().Name} is not a property or field"); + } + + private bool IsBackingField(MemberInfo member) + { + return member.Name.Contains("k__BackingField"); + } + + private bool IsShadowingProperty(MemberInfo member, Type parent) + { + if (member is not FieldInfo field) + return false; + var memberName = member.Name.ToLower().TrimStart('_'); + var shadowedField = parent.GetProperty(memberName, BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.IgnoreCase); + return shadowedField is not null; + } + + private string SerializeCollection(IEnumerable enumerable, int nestingLevel, int deepnessLevel, + List pathSegments) + { + var sb = new StringBuilder(); + sb.AppendLine(enumerable.GetType().Name); + sb.Append(new string(Tabchar, nestingLevel + 1)); + foreach (var obj in enumerable) + { + var objType = obj.GetType(); + pathSegments.Add(objType.Name); + var isKeyValuePair = objType.IsGenericType + && objType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>); + if (isKeyValuePair) + sb.Append(SerializeDictionaryElement(obj, deepnessLevel + 1, pathSegments)); + else + sb.Append(GetSerializerForType(obj.GetType()) + .Invoke(obj, nestingLevel + 1, deepnessLevel + 1, pathSegments)); + pathSegments.RemoveAt(pathSegments.Count - 1); + sb.Append(new string(Tabchar, nestingLevel + 1)); + } + sb.Remove(sb.Length - 1, 1); + return sb.ToString(); + } + + private string SerializeDictionaryElement(object obj, int deepnessLevel, List pathSegments) + { + if (IsDeepnessOverflow(deepnessLevel)) + return deepnessExceededString; + var type = obj.GetType(); + var key = type.GetProperty("Key")!.GetValue(obj); + var value = type.GetProperty("Value")!.GetValue(obj); + var sb = new StringBuilder(); + sb.Append(GetSerializerForType(key!.GetType()).Invoke(key, 0, + deepnessLevel + 1, pathSegments).TrimEnd(ResetCaretChar, NewLineChar, Tabchar)); + sb.Append(EqualityWithSpaces); + sb.Append(GetSerializerForType(value!.GetType()).Invoke(value, 0, + deepnessLevel + 1, pathSegments).TrimEnd(ResetCaretChar, NewLineChar, Tabchar)); + sb.Append(EnvironmentNewLine); + return sb.ToString(); + } + + private bool IsDeepnessOverflow(int deepnessLevel) + { + return deepnessLevel >= MaxDeepnessLevel; + } + + private string SerializePropertyOrField(MemberInfo memberInfo, object obj, int nestingLevel, + int deepnessLevel, List path) + { + var sb = new StringBuilder(); + var fullPropertyName = new PropertyPath(path); + if (IsPropertyNeedsUniqueSerialization(fullPropertyName, out var serializer)) + { + var newData = BuildSerializedField(memberInfo, serializer, obj, nestingLevel, deepnessLevel, path) + .TrimEnd(ResetCaretChar, NewLineChar, Tabchar); + sb.Append(newData + EnvironmentNewLine); + return sb.ToString(); + } + if (IsPropertyNeedsTrim(fullPropertyName, out var trimLen)) + { + var identation = new string(Tabchar, nestingLevel + 1); + var serializedInfo = BuildSerializedField(memberInfo, null, obj, nestingLevel, deepnessLevel, path); + var dataPrefixLength = identation.Length + memberInfo.Name.Length + EqualityWithSpaces.Length; + serializedInfo = serializedInfo.Substring(0, dataPrefixLength + trimLen) + .TrimEnd(ResetCaretChar, NewLineChar, Tabchar); + + sb.Append(serializedInfo + EnvironmentNewLine); + return sb.ToString(); + } + sb.Append(BuildSerializedField(memberInfo, null, obj, nestingLevel, deepnessLevel, path)); + return sb.ToString(); + } + + private string BuildSerializedField(MemberInfo memberInfo, SerializerDelegate? serializer, + object? obj, int nestingLevel, int deepnessLevel, List path) + { + var memberValue = GetMemberValue(memberInfo, obj); + if (memberValue is null) + return string.Empty; + var identation = new string(Tabchar, nestingLevel + 1); + var sb = new StringBuilder(); + sb.Append(identation + memberInfo.Name + EqualityWithSpaces); + if (serializer is null) + serializer = GetSerializerForType(memberValue.GetType()); + sb.Append(serializer.Invoke(GetMemberValue(memberInfo, obj), nestingLevel + 1, + deepnessLevel + 1, path)); + return sb.ToString(); + } + + private SerializerDelegate GetSerializerForType(Type type) + { + if (typeSerializers.TryGetValue(type, out var forType)) + return forType; + return Serialize; + } + + private bool IsPropertyNeedsUniqueSerialization(PropertyPath propertyName, + out SerializerDelegate? serializer) + { + serializer = null; + if (propertySerializers.TryGetValue(propertyName.ToString(), out var propertySerializer)) + { + serializer = propertySerializer; + return true; + } + return false; + } + + private bool IsPropertyNeedsTrim(PropertyPath fullPropertyName, out int trimLength) + { + trimLength = 0; + if (maxStringLength.TryGetValue(fullPropertyName.ToString(), out var value)) + { + trimLength = value; + return true; + } + return false; + } } } \ No newline at end of file diff --git a/ObjectPrinting/PropertyPath.cs b/ObjectPrinting/PropertyPath.cs new file mode 100644 index 000000000..6bcc73005 --- /dev/null +++ b/ObjectPrinting/PropertyPath.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace ObjectPrinting; + +public record PropertyPath +{ + private List pathSegments; + public PropertyPath(List pathSegments) => this.pathSegments = pathSegments; + + public override string ToString() => string.Join(".", pathSegments); + + public static PropertyPath ConvertFromSelector(Expression> selector) + { + var segments = new List(); + var expression = selector.Body; + + while (expression is MemberExpression member) + { + segments.Add(member.Member.Name); + expression = member.Expression; + } + segments.Add(typeof(TOwner).Name); + + segments.Reverse(); + return new PropertyPath(segments); + } +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfig.cs b/ObjectPrinting/PropertyPrintingConfig.cs new file mode 100644 index 000000000..db43cbc48 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfig.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq.Expressions; + +namespace ObjectPrinting; + +internal interface IPropertyPrintingConfigAccessor +{ + PrintingConfig ParentConfig { get; } + Expression>? PropertySelector { get; } + PropertyPath PropertyPath { get; } +} + +public class PropertyPrintingConfig : IPropertyPrintingConfigAccessor +{ + private readonly PrintingConfig config; + private readonly Expression>? selector; + private readonly PropertyPath propertyPath; + + public PropertyPrintingConfig(PrintingConfig config, Expression>? selector) + { + this.config = config; + this.selector = ValidateExpression(selector); + this.propertyPath = PropertyPath.ConvertFromSelector(selector!); // Не null т.к. валидируем это + } + + private Expression> ValidateExpression(Expression>? validatingSelector) + { + if (validatingSelector == null) + throw new ArgumentNullException(nameof(validatingSelector)); + if (validatingSelector.Body is not MemberExpression memberExpression) + throw new ArgumentException($"{nameof(validatingSelector)} must select property or field"); + if (!IsDirectPropertyAccess(memberExpression, validatingSelector.Parameters[0])) + throw new ArgumentException("Property selector should only access properties of the parameter, not external variables"); + return validatingSelector; + } + + private bool IsDirectPropertyAccess(Expression? expression, ParameterExpression parameter) + { + while (expression is MemberExpression member) + expression = member.Expression; + return expression == parameter; + } + + PrintingConfig IPropertyPrintingConfigAccessor.ParentConfig => config; + + Expression>? IPropertyPrintingConfigAccessor.PropertySelector => selector; + + PropertyPath IPropertyPrintingConfigAccessor.PropertyPath => + propertyPath; +} \ No newline at end of file diff --git a/ObjectPrinting/PropertyPrintingConfigExtensions.cs b/ObjectPrinting/PropertyPrintingConfigExtensions.cs new file mode 100644 index 000000000..bccec6e79 --- /dev/null +++ b/ObjectPrinting/PropertyPrintingConfigExtensions.cs @@ -0,0 +1,29 @@ +using System; + +namespace ObjectPrinting; + +public static class PropertyPrintingConfigExtensions +{ + public static PrintingConfig SetSerializationStyle + (this PropertyPrintingConfig propertyConfig, PrintingConfig.SerializerDelegate serializer) + { + var accessor = (IPropertyPrintingConfigAccessor)propertyConfig; + var propertyName = PropertyPath.ConvertFromSelector(accessor.PropertySelector!); + return accessor.ParentConfig.WithPropertySerialization(propertyName, serializer); + } + + public static PrintingConfig TrimStringToLength + (this PropertyPrintingConfig propertyConfig, int length) + { + var accessor = (IPropertyPrintingConfigAccessor)propertyConfig; + return accessor.ParentConfig.TrimStringToLength(accessor.PropertySelector!, length); + } + + public static PrintingConfig Exclude + (this PropertyPrintingConfig propertyConfig) + { + var accessor = (IPropertyPrintingConfigAccessor)propertyConfig; + var propertyName = PropertyPath.ConvertFromSelector(accessor.PropertySelector!); + return accessor.ParentConfig.Exclude(propertyName); + } +} \ No newline at end of file diff --git a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs index 4c8b2445c..31ba26f40 100644 --- a/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs +++ b/ObjectPrinting/Tests/ObjectPrinterAcceptanceTests.cs @@ -1,4 +1,10 @@ -using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Text; +using FluentAssertions; +using NUnit.Framework; namespace ObjectPrinting.Tests { @@ -6,22 +12,346 @@ namespace ObjectPrinting.Tests public class ObjectPrinterAcceptanceTests { [Test] - public void Demo() + public void DemonstrationTest() { - var person = new Person { Name = "Alex", Age = 19 }; + var person = new Person { Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + person.Phone = new Phone {Name = "Смартфон Vivo", Owner = person}; + PrintingConfig.SerializerDelegate phoneSerializer + = (obj, nestingLevel, deepnessLevel, path) => "MyPhone" + Environment.NewLine; - var printer = ObjectPrinter.For(); + var printer = ObjectPrinter.For() //1. Исключить из сериализации свойства определенного типа + .ExcludePropertyOfType() //2. Указать альтернативный способ сериализации для определенного типа + .WithTypeSerializationStyle(phoneSerializer) //3. Для числовых типов указать культуру + .SetTypeCulture(CultureInfo.InvariantCulture) //4. Настроить сериализацию конкретного свойства + .GetPropertyByName(x => x.Name) + .SetSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "MyName") //5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) - //6. Исключить из сериализации конкретного свойства + .GetPropertyByName(x => x.Surname).TrimStringToLength(3) + //6. Исключить из сериализации конкретное свойства + .GetPropertyByName(x => x.Id).Exclude(); string s1 = printer.PrintToString(person); + Console.WriteLine(s1); - //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + //7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию + var s2 = person.PrintToString(); + Console.WriteLine("\n\n\n" + s2); //8. ...с конфигурированием + var s3 = person.PrintToString(x => x.GetPropertyByName(p => p.Name).TrimStringToLength(3)); + Console.WriteLine("\n\n\n" + s3); + } + + [Test] + public void ExcludeType_ShouldRemove_PropertiesWithThisType() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var printer = ObjectPrinter + .For() + .ExcludePropertyOfType() + .ExcludePropertyOfType() + .ExcludePropertyOfType() + .ExcludePropertyOfType(); + + var expected = + "Person\r\n" + + "\tName = Alex\r\n" + + "\tSurname = Smith\r\n"; + + var ans = printer.PrintToString(person); + ans.Should().Be(expected); + } + + [Test] + public void WithTypeSerializationStyle_ShouldSetUniqueSerializationStyle_ToPropertiesWithThisType() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + person.Phone = new Phone {Name = "Смартфон Vivo"}; + var printer = ObjectPrinter + .For() + .WithTypeSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "999999999" + Environment.NewLine) + .WithTypeSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "My phone" + Environment.NewLine); + var ans = printer.PrintToString(person); + + var expected = + "Person\r\n\t" + + "Id = 999999999\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Phone = My phone\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void SetTypeCulture_ShouldSetTypeCulture_ToPropertiesWithThisType() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var printer = ObjectPrinter + .For() + .SetTypeCulture(CultureInfo.CurrentCulture); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + // [Test] + // public void GetPropertyByName_ShouldReturn_PropertyInfoByName() + // { + // var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + // var printer = ObjectPrinter + // .For() + // .GetPropertyByName(x => x.Id); + // var ans = printer.PropertyName; + // + // var expected = "Id"; + // + // ans.Should().Be(expected); + // } + + [Test] + public void SetSerializationStyle_ShouldSetUniqueSerializationStyle_ToGotProperty() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var printer = ObjectPrinter + .For() + .GetPropertyByName(x => x.Name) + .SetSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "My name" + Environment.NewLine); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = My name\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void TrimStringToLength_ShouldTrimPropertyLength_ToGotProperty() + { + var person = new Person {Name = "Alex", Surname = "Very long surname", Age = 19, Height = 180.5}; + var printer = ObjectPrinter + .For() + .GetPropertyByName(x => x.Surname).TrimStringToLength(5); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Very \r\n\t" + + "Height = 180,5\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void Exclude_ShouldRemove_GotProperty() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var printer = ObjectPrinter + .For() + .GetPropertyByName(x => x.Phone).Exclude(); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void PrivateFieldsAndProperties_ShouldBeSerialized() + { + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var secretCode = "1"; + person.SetSecretCode(secretCode); + person.PassportName = "passport"; + var printer = ObjectPrinter + .For() + .GetPropertyByName(x => x.Phone).Exclude(); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "PassportName = passport\r\n\t" + + "Age = 19\r\n\t" + + $"SecretCode = {secretCode}\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void SetSerializeStyle_ForFieldOrProperty_ShouldAffect_OnlyOnThisFieldOrProperty() + { + var person = new Person {Name = "Alex", Surname = "Smith", Height = 180.5, Age = 19}; + var phone = new Phone {Name = "Phone"}; + person.Phone = phone; + var printer = ObjectPrinter + .For() + .GetPropertyByName(x => x.Name) + .SetSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "My name"); + var ans = printer.PrintToString(person); + + var baseGuid = Guid.Empty.ToString(); + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = My name\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Phone = Phone\r\n\t\t" + + $"Id = {baseGuid}\r\n\t\t" + + "Name = Phone\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void LoopReferencesTest() + { + var baseGuid = Guid.Empty.ToString(); + var person = new Person {Name = "Alex", Surname = "Smith", Age = 19, Height = 180.5}; + var phone = new Phone {Name = "My phone", Owner = person}; + person.Phone = phone; + var printer = ObjectPrinter + .For(); + var ans = printer.PrintToString(person); + + var expected = + "Person\r\n\t" + + $"Id = {baseGuid}\r\n\t" + + "Name = Alex\r\n\t" + + "Surname = Smith\r\n\t" + + "Height = 180,5\r\n\t" + + "Phone = Phone\r\n\t\t" + + $"Id = {baseGuid}\r\n\t\t" + + "Name = My phone\r\n\t\t" + + "Owner = Person\r\n\t\t\t" + + $"Id = {baseGuid}\r\n\t\t\t" + + "Name = Alex\r\n\t\t\t" + + "Surname = Smith\r\n\t\t\t" + + "Height = 180,5\r\n\t\t\t" + + "Phone = Deepness exceeded!\r\n\t\t\t" + + "Age = 19\r\n\t" + + "Age = 19\r\n"; + + ans.Should().Be(expected); + } + + [Test] + public void Array_ShouldBeSerializedAs_JSON() + { + var array = new int[]{1, 2, 3}; + var printer = ObjectPrinter + .For() + .WithTypeSerializationStyle((obj, nestingLevel, deepnessLevel, path) => + obj.GetType() + Environment.NewLine); + var ans = printer.PrintToString(array); + + var expected = new StringBuilder(); + expected.Append("Int32[]\r\n"); + foreach(var i in array) + expected.Append("\t" + i.GetType() + "\r\n"); + + ans.Should().Be(expected.ToString()); + } + + [Test] + public void List_ShouldBeSerializedAs_JSON() + { + var list = new List {1, 2, 3}; + var printer = ObjectPrinter + .For>(); + var ans = printer.PrintToString(list); + + var expected = new StringBuilder(); + expected.Append("List`1\r\n"); + foreach(var i in list) + expected.Append("\t" + i.ToString() + "\r\n"); + + ans.Should().Be(expected.ToString()); + } + + [Test] + public void Dictionary_ShouldBeSerializedAs_JSON() + { + var dict = new Dictionary + { + {1, "one"}, + {2, "two"}, + {3, "three"} + }; + var printer = ObjectPrinter + .For>() + .WithTypeSerializationStyle((obj, nestingLevel, deepnessLevel, path) => "number"); + var ans = printer.PrintToString(dict); + + var expected = new StringBuilder(); + expected.Append("Dictionary`2\r\n"); + foreach(var i in dict) + expected.Append("\t" + "number" + " = " + i.Value + "\r\n"); + + ans.Should().Be(expected.ToString()); + } + + [Test] + public void PropertyPrintingConfig_ShouldValidate_NullSelector() + { + var config = new PrintingConfig(); + Action test = () => new PropertyPrintingConfig(config, null); + test.Should().Throw(); + } + + [Test] + public void PropertyPrintingConfig_ShouldValidate_NotFieldOrPropertySelector() + { + var config = new PrintingConfig(); + Action test = () => new PropertyPrintingConfig(config, a => "aaaa"); + test.Should().Throw(); + } + + [Test] + public void PropertyPrintingConfig_ShouldValidate_ExternalVariables() + { + var sd = new Rectangle(); + var config = new PrintingConfig(); + + Action test = () => new PropertyPrintingConfig(config, x => sd.Bottom); + test.Should().Throw(); } } } \ No newline at end of file diff --git a/ObjectPrinting/Tests/Person.cs b/ObjectPrinting/Tests/Person.cs index f95559554..bb4fc14c0 100644 --- a/ObjectPrinting/Tests/Person.cs +++ b/ObjectPrinting/Tests/Person.cs @@ -6,7 +6,26 @@ public class Person { public Guid Id { get; set; } public string Name { get; set; } + public string Surname { get; set; } public double Height { get; set; } - public int Age { get; set; } + public Phone Phone { get; set; } + public int Age; + private string SecretCode; + private string _passportName; + + public string PassportName + { + get => _passportName; + set => _passportName = value; + } + + public void SetSecretCode(string code) => this.SecretCode = code; + } + + public class Phone + { + public Guid Id { get; set; } + public string Name { get; set; } + public Person Owner { 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