Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Шевырин Никита #225

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,6 @@ _Pvt_Extensions
.fake/

.idea/

# Verify.NUnit
*.received.*
8 changes: 8 additions & 0 deletions ObjectPrinting/IPropertySerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ObjectPrinting;

public interface IPropertySerializer<TOwner, TProperty>
{
public PrintingConfig<TOwner> Use(Func<TProperty, string> converter);
}
8 changes: 8 additions & 0 deletions ObjectPrinting/ITypeSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ObjectPrinting;

public interface ITypeSerializer<TParam, TOwner>
{
public PrintingConfig<TOwner> Use(Func<TParam, string> converter);
}
17 changes: 17 additions & 0 deletions ObjectPrinting/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace ObjectPrinting;

public static class ObjectExtensions
{
public static string PrintToString<T>(this T obj)
{
return ObjectPrinter.For<T>().PrintToString(obj);
}

public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> configurer)
{
var config = configurer(ObjectPrinter.For<T>());
return config.PrintToString(obj);
}
}
2 changes: 2 additions & 0 deletions ObjectPrinting/ObjectPrinting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Verify.NUnit" Version="28.3.2" />
</ItemGroup>
</Project>
213 changes: 195 additions & 18 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,218 @@
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<TOwner>
{
private readonly List<Type> _excludedTypes = new();
private readonly List<MemberInfo> _excludedProperties = new();
private readonly Dictionary<Type, Delegate> _typeConverters = new();
private readonly Dictionary<MemberInfo, Delegate> _propertyConverters = new();
private readonly Dictionary<Type, CultureInfo> _cultureSpecs = new();
private readonly Dictionary<MemberInfo, int> _stringPropertyLengths = new();
internal int MaxStringLength { get; set; } = int.MaxValue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас ты ограничиваешь длину сразу для всех типов, но что, если мы захотим ограничить длину у одного конкретного поля? Сейчас твоя реализация не позволит такого сделать

Так же мне не кажется, что задание значения по умолчанию является удачным выбором. Число действительно большое, но это ограничение, которого без указания пользователем по идее быть не должно, в теории на вход может прийти строка, длина которой превосходит int.MaxValue, и тогда мы без предупреждения обрежем строку. Если пользователь явно не указал, то обрезать строку не надо

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вообще, если сослаться на исходник класса String в .NET, то там можно увидеть строку internal const int MaxLength = 0x3FFFFFDF;, в которой указано максимально возможная длина строки, и это значение сильно меньше чем int.MaxValue. Также если попытаться создать строку длины, большей, чем MaxLength, например так: var str = new string('a', 0x3FFFFFDF + 1);. То код падает с System.OutOfMemoryException

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да, ты прав, у строки действительно не может быть длины, превышающей это значение. Но мне все равно не кажется хорошей идея задавать потолок в поле, без явной на то необходимости. Как мне кажется, лучше было бы сделать поле нулабельным, и если значение на ограничение не устанавлено, то не обрезать строку вовсе

private int MaxRecursionDepth { get; set; } = 16;

public string PrintToString(TOwner obj)
{
return PrintToString(obj, 0);
return PrintToString(obj, 0, 0);
}

private string PrintToString(object obj, int nestingLevel)
internal void AddCultureSpec(Type type, CultureInfo cultureInfo)
{
//TODO apply configurations
if (obj == null)
return "null" + Environment.NewLine;
_cultureSpecs.Add(type, cultureInfo);
}

var finalTypes = new[]
{
typeof(int), typeof(double), typeof(float), typeof(string),
typeof(DateTime), typeof(TimeSpan)
};
if (finalTypes.Contains(obj.GetType()))
return obj + Environment.NewLine;
internal void AddStringPropertyLength(MemberInfo propertyInfo, int length)
{
_stringPropertyLengths.Add(propertyInfo, length);
}

public PrintingConfig<TOwner> WithMaxRecursionDepth(int maxRecursionDepth)
{
if (maxRecursionDepth < 0)
throw new ArgumentOutOfRangeException($"{nameof(maxRecursionDepth)} must not be less than 0");
MaxRecursionDepth = maxRecursionDepth;
return this;
}

internal void AddTypeConverter<TParam>(Type type, Func<TParam, string?> converter)
{
_typeConverters.Add(type, converter);
}

internal void AddPropertyConverter<TParam>(Func<TParam, string> converter, MemberInfo propertyInfo)
{
_propertyConverters.Add(propertyInfo, converter);
}

public PrintingConfig<TOwner> ExceptType<T>()
{
_excludedTypes.Add(typeof(T));
return this;
}

public PrintingConfig<TOwner> ExceptProperty(Expression<Func<TOwner, object>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException($"{nameof(propertyExpression)} cannot be null");

_excludedProperties.Add(GetMemberInfo(propertyExpression));
return this;
}

public ITypeSerializer<TParam, TOwner> ForType<TParam>()
{
return new TypeSerializerImpl<TParam, TOwner>(this);
}

public IPropertySerializer<TOwner, TProperty> ForProperty<TProperty>(
Expression<Func<TOwner, TProperty>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException($"{nameof(propertyExpression)} cannot be null");

return new PropertySerializerImpl<TOwner, TProperty>(this, GetMemberInfo(propertyExpression));
}

private static MemberInfo GetMemberInfo<TProperty>(Expression<Func<TOwner, TProperty>> propertyExpression)
{
if (propertyExpression.Body is MemberExpression memberExpression)
return memberExpression.Member;

if (propertyExpression.Body is UnaryExpression unaryExpression
&& unaryExpression.Operand is MemberExpression unaryMemberExpression)
return unaryMemberExpression.Member;

throw new ArgumentException("Expression does not refer to a property or field.");
}

private string PrintToString(object obj, int nestingLevel, int recursionDepth)
{
if (obj == null)
return "null";

if (obj is IFormattable formattable
&& _cultureSpecs.TryGetValue(formattable.GetType(), out var cultureSpec))
return $"{formattable.ToString(null, cultureSpec)}";

if (obj is string str)
return str.Substring(0, Math.Min(MaxStringLength, str.Length));

if (obj.GetType().IsValueType)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я не совсем это имел в виду, когда говорил про создание одной и той же коллекции при вызове PrintToString. Ее можно было оставить, просто вынести в статическое поле класса

return $"{obj}";

if (recursionDepth > MaxRecursionDepth)
return "null";

var identation = new string('\t', nestingLevel + 1);
var indentation = new string('\t', nestingLevel + 1);
var sb = new StringBuilder();
var type = obj.GetType();
sb.AppendLine(type.Name);
foreach (var propertyInfo in type.GetProperties())

if (obj is IDictionary dictionary)
return SerializeDictionary(sb, dictionary, nestingLevel, recursionDepth);

if (obj is IEnumerable enumerable)
return SerializeEnumerable(sb, enumerable, nestingLevel, recursionDepth);

var bracketIndentation = new string('\t', nestingLevel);
sb.AppendLine($"{type.Name}:");
sb.AppendLine($"{bracketIndentation}{{");
var properties = type.GetProperties();
foreach (var propertyInfo in properties)
{
if (!_excludedProperties.Contains(propertyInfo) && !_excludedTypes.Contains(propertyInfo.PropertyType))
{
var valueString = GetValueString(propertyInfo, obj, nestingLevel, recursionDepth);
sb.AppendLine($"{indentation}{propertyInfo.Name} = {valueString}");
}
}
sb.Append($"{bracketIndentation}}}");
return sb.ToString();
}

private string SerializeEnumerable(StringBuilder sb, IEnumerable enumerable, int nestingLevel, int recursionDepth)
{
var bracketIndentation = new string('\t', nestingLevel);
sb.AppendLine($"[");
if (!enumerable.GetEnumerator().MoveNext())
{
sb.Append(identation + propertyInfo.Name + " = " +
PrintToString(propertyInfo.GetValue(obj),
nestingLevel + 1));
return "[]";
}
enumerable.GetEnumerator().Reset();
foreach (var element in enumerable)
{
sb.Append($"{bracketIndentation}\t");
var valueString = String.Empty;
if (_typeConverters.TryGetValue(element.GetType(), out var typeConverter))
valueString =
$"{typeConverter.DynamicInvoke(element) as string ?? "null"}";
else
valueString = PrintToString(element, nestingLevel + 1, recursionDepth + 1);
sb.AppendLine($"{valueString},");
}
sb.Append($"{bracketIndentation}]");
return sb.ToString();
}

private string SerializeDictionary(StringBuilder sb, IDictionary dictionary, int nestingLevel, int recursionDepth)
{
var bracketIndentation = new string('\t', nestingLevel);
sb.AppendLine($"[");
foreach (DictionaryEntry element in dictionary)
{
var key = element.Key;
var value = element.Value;

var keyValueIndentation = new string('\t', nestingLevel + 1);
sb.AppendLine($"{bracketIndentation}{{");
sb.Append($"{keyValueIndentation}key: ");
var keyString = String.Empty;
if (_typeConverters.TryGetValue(key.GetType(), out var typeConverter))
keyString =
$"{typeConverter.DynamicInvoke(key) as string ?? "null"}";
else
keyString = PrintToString(key, nestingLevel + 2, recursionDepth + 1);
sb.AppendLine($"{keyString}");
sb.Append($"{keyValueIndentation}value: ");
var valueString = String.Empty;
if (_typeConverters.TryGetValue(value.GetType(), out typeConverter))
valueString =
$"{typeConverter.DynamicInvoke(value) as string ?? "null"}";
else
valueString = PrintToString(value, nestingLevel + 2, recursionDepth + 1);
sb.AppendLine($"{valueString}");
sb.AppendLine($"{bracketIndentation}}},");
}
sb.AppendLine($"{bracketIndentation}]");
return sb.ToString();
}

private string GetValueString(PropertyInfo propertyInfo, object obj, int nestingLevel, int recursionDepth)
{
var propertyValue = propertyInfo.GetValue(obj);
if (propertyValue == null || !TryConvert(propertyInfo, propertyValue, out var valueString))
valueString = PrintToString(propertyValue, nestingLevel + 1, recursionDepth + 1);
return valueString;
}

private bool TryConvert(PropertyInfo propertyInfo, object? propertyValue, out string value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется, что если заинлайнить этот метод, то код станет проще и чище

{
value = String.Empty;
if (_propertyConverters.TryGetValue(propertyInfo, out var converter))
value = $"{converter.DynamicInvoke(propertyValue) as string ?? "null"}";
else if (propertyValue is string str && _stringPropertyLengths.TryGetValue(propertyInfo, out var length))
value = $"{str.Substring(0, Math.Min(length, str.Length))}";
else if (_typeConverters.TryGetValue(propertyInfo.PropertyType, out var typeConverter))
value = $"{typeConverter.DynamicInvoke(propertyValue) as string ?? "null"}";
return value != String.Empty;
}
}
}
18 changes: 18 additions & 0 deletions ObjectPrinting/PropertySerializerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace ObjectPrinting;

public static class PropertySerializerExtensions
{
public static PrintingConfig<TOwner> UseMaxLength<TOwner>(
this IPropertySerializer<TOwner, string> serializer,
int maxLength)
{
if (maxLength < 0)
throw new ArgumentOutOfRangeException($"{nameof(maxLength)} cannot be negative");
var typeSerializer = (PropertySerializerImpl<TOwner, string>)serializer;
var config = typeSerializer.Config;
config.AddStringPropertyLength(typeSerializer.MemberInfo, maxLength);
return config;
}
}
22 changes: 22 additions & 0 deletions ObjectPrinting/PropertySerializerImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Reflection;

namespace ObjectPrinting;

internal class PropertySerializerImpl<TOwner, TProperty> : IPropertySerializer<TOwner, TProperty>
{
public PrintingConfig<TOwner> Config { get; }
public readonly MemberInfo MemberInfo;

internal PropertySerializerImpl(PrintingConfig<TOwner> config, MemberInfo memberInfo)
{
Config = config;
MemberInfo = memberInfo;
}

public PrintingConfig<TOwner> Use(Func<TProperty, string> converter)
{
Config.AddPropertyConverter(converter, MemberInfo);
return Config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
1,
2,
3.14,
4,
5,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
key: a
value: 1.1
},
{
key: b
value: 2.2
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
Alex,
John,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Person:
{
Id = 00000000-0000-0000-0000-000000000000
Name = Alex
Surname = Smith
Height = 177.4
Age = 19
OtherPerson = null
Persons = []
}
Loading