-
Notifications
You must be signed in to change notification settings - Fork 245
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
base: master
Are you sure you want to change the base?
Шевырин Никита #225
Changes from all commits
441f072
bdadc67
0a315e1
9605d4a
54a9708
360534d
12332f2
9e2a950
c7b2f79
02918de
c87c057
92dbb2a
d6aeaa8
50334f7
743b6a8
69eca2a
90176f4
d79b36a
436cdd4
3c8a091
aea11ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,3 +236,6 @@ _Pvt_Extensions | |
.fake/ | ||
|
||
.idea/ | ||
|
||
# Verify.NUnit | ||
*.received.* |
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); | ||
} |
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); | ||
} |
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); | ||
} | ||
} |
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; | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
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; | ||
} | ||
} |
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 = [] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Сейчас ты ограничиваешь длину сразу для всех типов, но что, если мы захотим ограничить длину у одного конкретного поля? Сейчас твоя реализация не позволит такого сделать
Так же мне не кажется, что задание значения по умолчанию является удачным выбором. Число действительно большое, но это ограничение, которого без указания пользователем по идее быть не должно, в теории на вход может прийти строка, длина которой превосходит int.MaxValue, и тогда мы без предупреждения обрежем строку. Если пользователь явно не указал, то обрезать строку не надо
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Да, ты прав, у строки действительно не может быть длины, превышающей это значение. Но мне все равно не кажется хорошей идея задавать потолок в поле, без явной на то необходимости. Как мне кажется, лучше было бы сделать поле нулабельным, и если значение на ограничение не устанавлено, то не обрезать строку вовсе