-
Notifications
You must be signed in to change notification settings - Fork 245
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
250 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,156 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Text; | ||
|
||
namespace ObjectPrinting | ||
|
||
namespace ObjectPrinting; | ||
|
||
public class PrintingConfig<TOwner> | ||
{ | ||
public class PrintingConfig<TOwner> | ||
private readonly HashSet<Type> excludedTypes = []; | ||
private readonly HashSet<string> excludedProperties = []; | ||
private readonly Dictionary<Type, Func<object, string>> typeSerializers = new(); | ||
private readonly Dictionary<Type, CultureInfo> typeCultures = new(); | ||
private readonly Dictionary<string, Func<object, string>> propertySerializers = new(); | ||
|
||
public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>() => new(this); | ||
|
||
public PropertyPrintingConfig<TOwner, TPropType> Printing<TPropType>( | ||
Expression<Func<TOwner, TPropType>> memberSelector) => | ||
new(this, memberSelector); | ||
|
||
public PrintingConfig<TOwner> Excluding<TPropType>(Expression<Func<TOwner, TPropType>> memberSelector) | ||
{ | ||
if (memberSelector.Body is not MemberExpression memberExpression) | ||
throw new ArgumentException("Expression must be a member expression", nameof(memberSelector)); | ||
excludedProperties.Add(memberExpression.Member.Name); | ||
return this; | ||
} | ||
|
||
internal PrintingConfig<TOwner> Excluding<TPropType>() | ||
{ | ||
excludedTypes.Add(typeof(TPropType)); | ||
return this; | ||
} | ||
|
||
public string PrintToString(TOwner obj) => PrintToString(obj, 0); | ||
|
||
public void AddSerializerForType<TPropType>(Func<TPropType, string> serializer) => | ||
typeSerializers[typeof(TPropType)] = obj => serializer((TPropType)obj); | ||
|
||
public void AddSerializerForProperty(string propertyName, Func<object, string> serializer) => | ||
propertySerializers[propertyName] = serializer; | ||
|
||
public void AddCultureForType<TPropType>(CultureInfo culture) => typeCultures[typeof(TPropType)] = culture; | ||
|
||
|
||
|
||
private string PrintToString(object? obj, int nestingLevel, HashSet<object>? visitedObjects = null) | ||
{ | ||
public string PrintToString(TOwner obj) | ||
if (obj == null) | ||
return "null" + Environment.NewLine; | ||
|
||
visitedObjects ??= []; | ||
|
||
if (!visitedObjects.Add(obj)) | ||
return "Cyclic reference!" + Environment.NewLine; | ||
|
||
var finalTypes = new[] | ||
{ | ||
return PrintToString(obj, 0); | ||
typeof(int), typeof(double), typeof(float), typeof(string), | ||
typeof(DateTime), typeof(TimeSpan) | ||
}; | ||
|
||
var type = obj.GetType(); | ||
|
||
if (typeSerializers.TryGetValue(type, out var serializer)) | ||
return serializer(obj) + Environment.NewLine; | ||
|
||
if (typeCultures.TryGetValue(type, out var culture) && obj is IFormattable formattable) | ||
return formattable.ToString(null, culture) + Environment.NewLine; | ||
|
||
if (finalTypes.Contains(type)) | ||
return obj + Environment.NewLine; | ||
|
||
return obj switch | ||
{ | ||
IDictionary dictionary => SerializeDictionary(dictionary, nestingLevel, visitedObjects), | ||
IEnumerable enumerable => SerializeEnumerable(enumerable, nestingLevel, visitedObjects), | ||
_ => SerializeObject(obj, nestingLevel, visitedObjects) | ||
}; | ||
} | ||
|
||
private string SerializeDictionary(IDictionary dictionary, int nestingLevel, HashSet<object> visitedObjects) | ||
{ | ||
var dictionaryType = dictionary.GetType().Name; | ||
var indentation = new string('\t', nestingLevel + 1); | ||
var sb = new StringBuilder(); | ||
|
||
sb.AppendLine($"{dictionaryType}:"); | ||
|
||
foreach (DictionaryEntry entry in dictionary) | ||
{ | ||
sb.Append(indentation + "Key = " + | ||
PrintToString(entry.Key, nestingLevel + 1, visitedObjects)); | ||
sb.Append(indentation + "Value = " + | ||
PrintToString(entry.Value, nestingLevel + 1, visitedObjects)); | ||
} | ||
|
||
private string PrintToString(object obj, int nestingLevel) | ||
visitedObjects.Remove(dictionary); | ||
return sb.ToString(); | ||
} | ||
|
||
private string SerializeEnumerable(IEnumerable enumerable, int nestingLevel, HashSet<object> visitedObjects) | ||
{ | ||
var collectionType = enumerable.GetType().Name; | ||
var indentation = new string('\t', nestingLevel + 1); | ||
var sb = new StringBuilder(); | ||
|
||
sb.AppendLine($"{collectionType}:"); | ||
|
||
foreach (var element in enumerable) | ||
{ | ||
//TODO apply configurations | ||
if (obj == null) | ||
return "null" + Environment.NewLine; | ||
sb.Append(indentation + "- " + | ||
PrintToString(element, nestingLevel + 1, visitedObjects)); | ||
} | ||
|
||
visitedObjects.Remove(enumerable); | ||
return sb.ToString(); | ||
} | ||
|
||
private string SerializeObject(object obj, int nestingLevel, HashSet<object> visitedObjects) | ||
{ | ||
var type = obj.GetType(); | ||
var indentation = new string('\t', nestingLevel + 1); | ||
var sb = new StringBuilder(); | ||
sb.AppendLine(type.Name); | ||
|
||
var finalTypes = new[] | ||
foreach (var propertyInfo in type.GetProperties()) | ||
{ | ||
if (excludedTypes.Contains(propertyInfo.PropertyType) || | ||
excludedProperties.Contains(propertyInfo.Name)) | ||
continue; | ||
|
||
var propertyName = propertyInfo.Name; | ||
var propertyValue = propertyInfo.GetValue(obj); | ||
|
||
if (propertySerializers.TryGetValue(propertyName, out var propertySerializer)) | ||
{ | ||
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(indentation + propertyName + " = " + | ||
propertySerializer(propertyValue!) + Environment.NewLine); | ||
} | ||
else | ||
{ | ||
sb.Append(identation + propertyInfo.Name + " = " + | ||
PrintToString(propertyInfo.GetValue(obj), | ||
nestingLevel + 1)); | ||
sb.Append(indentation + propertyName + " = " + | ||
PrintToString(propertyValue, nestingLevel + 1, visitedObjects)); | ||
} | ||
return sb.ToString(); | ||
} | ||
|
||
visitedObjects.Remove(obj); | ||
return sb.ToString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System; | ||
using System.Globalization; | ||
using System.Linq.Expressions; | ||
|
||
namespace ObjectPrinting; | ||
|
||
public class PropertyPrintingConfig<TOwner, TPropType>(PrintingConfig<TOwner> printingConfig, | ||
Expression<Func<TOwner, TPropType>> memberSelector = null!) : IPropertyPrintingConfig<TOwner, TPropType> | ||
{ | ||
public Expression<Func<TOwner, TPropType>> MemberSelector => memberSelector; | ||
|
||
public PrintingConfig<TOwner> Using(Func<TPropType, string> print) | ||
{ | ||
printingConfig.AddSerializerForType(print); | ||
return printingConfig; | ||
} | ||
|
||
public PrintingConfig<TOwner> Using(CultureInfo culture) | ||
{ | ||
if (!typeof(TPropType).IsValueType || !typeof(IFormattable).IsAssignableFrom(typeof(TPropType))) | ||
throw new InvalidOperationException($"Can't apply culture for type: {typeof(TPropType)}"); | ||
|
||
printingConfig.AddCultureForType<TPropType>(culture); | ||
return printingConfig; | ||
} | ||
|
||
PrintingConfig<TOwner> IPropertyPrintingConfig<TOwner, TPropType>.ParentConfig => printingConfig; | ||
} | ||
|
||
public interface IPropertyPrintingConfig<TOwner, TPropType> | ||
{ | ||
PrintingConfig<TOwner> ParentConfig { get; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System; | ||
using System.Linq.Expressions; | ||
|
||
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 PropertyPrintingConfig<TOwner, string> propConfig, int maxLen) | ||
{ | ||
if (maxLen < 0) | ||
throw new ArgumentOutOfRangeException(nameof(maxLen), "Max length must be non-negative!"); | ||
|
||
IPropertyPrintingConfig<TOwner, string> propertyConfig = propConfig; | ||
var printingConfig = propertyConfig.ParentConfig; | ||
var propertyName = ((MemberExpression)propConfig.MemberSelector.Body).Member.Name; | ||
|
||
printingConfig.AddSerializerForProperty(propertyName, value => | ||
{ | ||
var val = value as string ?? string.Empty; | ||
return val.Length > maxLen ? val[..maxLen] : val; | ||
}); | ||
|
||
return printingConfig; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,36 @@ | ||
using NUnit.Framework; | ||
using System; | ||
using System.Globalization; | ||
using NUnit.Framework; | ||
|
||
namespace ObjectPrinting.Tests | ||
namespace ObjectPrinting.Tests; | ||
|
||
[TestFixture] | ||
public class ObjectPrinterAcceptanceTests | ||
{ | ||
[TestFixture] | ||
public class ObjectPrinterAcceptanceTests | ||
[Test] | ||
public void Demo() | ||
{ | ||
[Test] | ||
public void Demo() | ||
{ | ||
var person = new Person { Name = "Alex", Age = 19 }; | ||
var person = new Person { Name = "Alex", Age = 19 }; | ||
|
||
var printer = ObjectPrinter.For<Person>(); | ||
//1. Исключить из сериализации свойства определенного типа | ||
//2. Указать альтернативный способ сериализации для определенного типа | ||
//3. Для числовых типов указать культуру | ||
//4. Настроить сериализацию конкретного свойства | ||
//5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) | ||
//6. Исключить из сериализации конкретного свойства | ||
|
||
string s1 = printer.PrintToString(person); | ||
var printer = ObjectPrinter.For<Person>() | ||
//1. Исключить из сериализации свойства определенного типа | ||
.Excluding<Guid>() | ||
//2. Указать альтернативный способ сериализации для определенного типа | ||
.Printing<int>().Using(i => i.ToString("X")) | ||
//3. Для числовых типов указать культуру | ||
.Printing<double>().Using(CultureInfo.InvariantCulture) | ||
//4. Настроить сериализацию конкретного свойства | ||
.Printing(p => p.Name) | ||
//5. Настроить обрезание строковых свойств (метод должен быть виден только для строковых свойств) | ||
.TrimmedToLength(10) | ||
//6. Исключить из сериализации конкретного свойства | ||
.Excluding(p => p.Age); | ||
|
||
var s1 = printer.PrintToString(person); | ||
|
||
//7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию | ||
//8. ...с конфигурированием | ||
} | ||
//7. Синтаксический сахар в виде метода расширения, сериализующего по-умолчанию | ||
var s2 = person.PrintToString(); | ||
//8. ...с конфигурированием | ||
var s3 = person.PrintToString(s => s.Excluding(p => p.Age)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,11 @@ | ||
using System; | ||
|
||
namespace ObjectPrinting.Tests | ||
namespace ObjectPrinting.Tests; | ||
|
||
public class Person | ||
{ | ||
public class Person | ||
{ | ||
public Guid Id { get; set; } | ||
public string Name { get; set; } | ||
public double Height { get; set; } | ||
public int Age { get; set; } | ||
} | ||
public Guid Id { get; set; } | ||
public string Name { get; set; } | ||
public double Height { get; set; } | ||
public int Age { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters