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

Минеев Максим #233

Open
wants to merge 5 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
6 changes: 6 additions & 0 deletions ObjectPrinting/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ObjectPrinting;

public static class ObjectExtensions
{
public static string PrintToString<T>(this T obj) => ObjectPrinter.For<T>().PrintToString(obj);
}
12 changes: 4 additions & 8 deletions ObjectPrinting/ObjectPrinter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
namespace ObjectPrinting
namespace ObjectPrinting;

public static class ObjectPrinter
{
public class ObjectPrinter
{
public static PrintingConfig<T> For<T>()
{
return new PrintingConfig<T>();
}
}
public static PrintingConfig<T> For<T>() => new();
}
6 changes: 0 additions & 6 deletions ObjectPrinting/ObjectPrinting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,4 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
</Project>
211 changes: 176 additions & 35 deletions ObjectPrinting/PrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,182 @@
using System;
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
namespace ObjectPrinting;

public class PrintingConfig<TOwner>
{
public class PrintingConfig<TOwner>
{
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();
}
internal readonly Dictionary<Type, Func<object, string>> TypePrinters = new();
internal readonly Dictionary<string, Func<object, string>> PropertyPrinters = new();
internal IFormatProvider Culture = CultureInfo.InvariantCulture;
internal readonly Dictionary<string, int> MaxLengths = new();
private readonly HashSet<Type> excludedTypes = [];
private readonly HashSet<string> excludedProperties = [];

private readonly HashSet<Type> finalTypes =
[
typeof(int), typeof(double), typeof(float), typeof(DateTime), typeof(TimeSpan), typeof(string), typeof(bool),
typeof(long)
];

public ITypePrintingConfig<TOwner, TType> Printing<TType>() =>
new TypePrintingConfig<TOwner, TType>(this);

public IPropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>(
Expression<Func<TOwner, TPropType>> memberSelector)
{
var propertyName = GetPropertyName(memberSelector);
return new PropertyPrintingConfig<TOwner, TPropType>(this, propertyName);
}

public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
var propertyName = GetPropertyName(memberSelector);
excludedProperties.Add(propertyName);
return this;
}

public PrintingConfig<TOwner> Excluding<TPropType>()
{
excludedTypes.Add(typeof(TPropType));
return this;
}

public string PrintToString(TOwner obj) => PrintToString(obj, 0);

private static string GetPropertyName<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector)
{
if (memberSelector.Body is not MemberExpression member)
throw new ArgumentException("Expression must be a property access.");

return member.Member.Name;
}

private string PrintToString(object? obj, int nestingLevel, Stack<string>? pathStack = null)
{
if (obj == null) return "null";
var identation = new string('\t', nestingLevel);

var type = obj.GetType();
pathStack ??= new Stack<string>();
if (IsCycleDependency(obj, type, pathStack))
return identation + $"Cycle dependency: {obj}";
pathStack.Push(type.FullName + obj.GetHashCode());
if (finalTypes.Contains(obj.GetType())) return obj.ToString() ?? string.Empty;

var sb = new StringBuilder();
sb.Append(identation + type.Name);
if (obj is not string && type.IsAssignableTo(typeof(IEnumerable)))
return PrintEnumerable((IEnumerable)obj, nestingLevel, pathStack);

sb.AppendLine(PrintMembersToString(obj.GetType().GetFields(),
field => ((FieldInfo)field).GetValue(obj), nestingLevel, pathStack));
sb.Append(PrintMembersToString(obj.GetType().GetProperties(),
prop => ((PropertyInfo)prop).GetValue(obj), nestingLevel, pathStack));
pathStack.Pop();
return sb.ToString();
}

private bool IsCycleDependency(object? obj, Type type, Stack<string> pathStack)
=> type != typeof(string) &&
!finalTypes.Contains(type) &&
pathStack.Contains(type.FullName + obj?.GetHashCode());

private string PrintMembersToString(IEnumerable<MemberInfo> members, Func<MemberInfo, object?> getValue,
int nestingLevel, Stack<string> pathStack)
{
var identation = new string('\t', nestingLevel + 1);
var items = (from memberInfo in members
let value = getValue(memberInfo)
let type = memberInfo is PropertyInfo prop ? prop.PropertyType : ((FieldInfo)memberInfo).FieldType
select Serialize(value, type, memberInfo.Name, nestingLevel + 1, pathStack)
into serializedMember
where !string.IsNullOrEmpty(serializedMember)
select identation + serializedMember).ToList();

return string.Join(",\n", items);
}

private string Serialize<T>(T valueToSerialize, Type type, string name, int level, Stack<string> pathStack)
{
if (excludedTypes.Contains(type) || excludedProperties.Contains(name)) return string.Empty;
if (valueToSerialize is null) return name + " = " + "null";

var value = "";
if (type.IsAssignableTo(typeof(IFormattable)))
value = ((IFormattable)valueToSerialize).ToString(null, Culture);
if (PropertyPrinters.TryGetValue(name, out var propertyPrinter))
value = propertyPrinter(valueToSerialize);
if (TypePrinters.TryGetValue(type, out var typePrinter))
value = typePrinter(valueToSerialize);
if (type.IsAssignableTo(typeof(IEnumerable)))
value = PrintEnumerable((IEnumerable)valueToSerialize, level, pathStack);
if (type.IsAssignableTo(typeof(KeyValuePair<,>)))
value = PrintKeyValuePair(valueToSerialize, pathStack);
if (type.IsAssignableTo(typeof(IDictionary)))
value = PrintDictionary((IDictionary)valueToSerialize, level, pathStack);
if (valueToSerialize is string stringValue && MaxLengths.TryGetValue(name, out var maxLength))
value = stringValue[..Math.Min(value.Length, maxLength)];
if (value == "")
value = PrintToString(valueToSerialize, level + 1);
if (value.Length > 0)
return name + " = " + value;
return string.Empty;
}

private string PrintEnumerable(IEnumerable? enumerable, int nestingLevel, Stack<string> pathStack)
{
var bracketIdentation = new string('\t', nestingLevel);
var itemIdentation = new string('\t', nestingLevel + 1);
if (enumerable is null) return "null";
if (enumerable is string) return enumerable.ToString() ?? string.Empty;
if (enumerable is IDictionary dictionary) return PrintDictionary(dictionary, nestingLevel, pathStack);
var elementType = GetElementType(enumerable);
if (elementType.IsAssignableTo(typeof(IEnumerable)))
return
$"[\n{string.Join(",\n", enumerable.Cast<object>().Select(item
=> itemIdentation + PrintEnumerable((IEnumerable)item, nestingLevel + 1, pathStack)))}\n{bracketIdentation}]";
if (finalTypes.Contains(elementType))
return $"[{string.Join(", ", enumerable.Cast<object>().Select(item => item.ToString() ?? string.Empty))}]";
var result =
$"[\n{string.Join(",\n", enumerable.Cast<object>().Select(item
=> PrintToString(item, nestingLevel + 1, pathStack)))}\n{bracketIdentation}]";
return result;
}

private string PrintDictionary(IDictionary? dictionary, int nestingLevel, Stack<string> pathStack)
{
if (dictionary is null) return "null";
if (dictionary.Count == 0) return "{}";
var bracketIdentation = new string('\t', nestingLevel);
var itemIdentation = new string('\t', nestingLevel + 1);
var serializedItems = dictionary.Keys.Cast<object>()
.Select(key =>
$"{PrintToString(key, nestingLevel + 1)}: {PrintToString(dictionary[key], nestingLevel + 1, pathStack)}");
var result =
$"{{\n{itemIdentation}{string.Join($",\n{itemIdentation}", serializedItems)}\n{bracketIdentation}}}";
return result;
}

private string PrintKeyValuePair(object keyValuePair, Stack<string> pathStack)
{
return keyValuePair is KeyValuePair<object, object> keyValue
? $"{PrintToString(keyValue.Key, 0, pathStack)} = {PrintToString(keyValue.Value, 0, pathStack)}"
: string.Empty;
}

private Type GetElementType(IEnumerable? enumerable)
{
if (enumerable is null) return typeof(object);
var enumerableType = enumerable.GetType()
.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));

return enumerableType?.GetGenericArguments().FirstOrDefault() ?? typeof(object);
}
}
25 changes: 25 additions & 0 deletions ObjectPrinting/PropertyPrintingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace ObjectPrinting;

public class PropertyPrintingConfig<TOwner, TPropType>(PrintingConfig<TOwner> printingConfig, string propertyName)
: IPropertyPrintingConfig<TOwner, TPropType>
{
public string PropertyName => propertyName;

public PrintingConfig<TOwner> Using(Func<TPropType, string> print)
{
if (!printingConfig.PropertyPrinters.TryAdd(propertyName, obj => print((TPropType)obj)))
printingConfig.PropertyPrinters[propertyName] = obj => print((TPropType)obj);
return printingConfig;
}

PrintingConfig<TOwner> IPropertyPrintingConfig<TOwner, TPropType>.ParentConfig => printingConfig;
}

public interface IPropertyPrintingConfig<TOwner, out TPropType>
{
string PropertyName { get; }
PrintingConfig<TOwner> ParentConfig { get; }
PrintingConfig<TOwner> Using(Func<TPropType, string> print);
}
17 changes: 17 additions & 0 deletions ObjectPrinting/PropertyPrintingConfigExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace ObjectPrinting;

public static class PropertyPrintingConfigExtensions
{
public static string PrintToString<T>(this T obj, Func<PrintingConfig<T>, PrintingConfig<T>> config)
=> config(ObjectPrinter.For<T>()).PrintToString(obj);

public static PrintingConfig<TOwner> TrimmedToLength<TOwner>(
this IPropertyPrintingConfig<TOwner, string> propConfig,
int maxLen)
{
propConfig.ParentConfig.MaxLengths.TryAdd(propConfig.PropertyName, maxLen);
return propConfig.ParentConfig;
}
}
10 changes: 0 additions & 10 deletions ObjectPrinting/Solved/ObjectExtensions.cs

This file was deleted.

10 changes: 0 additions & 10 deletions ObjectPrinting/Solved/ObjectPrinter.cs

This file was deleted.

62 changes: 0 additions & 62 deletions ObjectPrinting/Solved/PrintingConfig.cs

This file was deleted.

Loading