RaspberryIO/Unosquare.Swan.Lite/Formatters/Json.Converter.cs
2019-12-04 17:10:06 +01:00

302 lines
11 KiB
C#

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Unosquare.Swan.Attributes;
namespace Unosquare.Swan.Formatters {
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public static partial class Json {
private class Converter {
private static readonly ConcurrentDictionary<MemberInfo, String> MemberInfoNameCache =
new ConcurrentDictionary<MemberInfo, String>();
private static readonly ConcurrentDictionary<Type, Type> ListAddMethodCache = new ConcurrentDictionary<Type, Type>();
private readonly Object _target;
private readonly Type _targetType;
private readonly Boolean _includeNonPublic;
private Converter(
Object source,
Type targetType,
ref Object targetInstance,
Boolean includeNonPublic) {
this._targetType = targetInstance != null ? targetInstance.GetType() : targetType;
this._includeNonPublic = includeNonPublic;
if(source == null) {
return;
}
Type sourceType = source.GetType();
if(this._targetType == null || this._targetType == typeof(Object)) {
this._targetType = sourceType;
}
if(sourceType == this._targetType) {
this._target = source;
return;
}
if(!this.TrySetInstance(targetInstance, source, ref this._target)) {
return;
}
this.ResolveObject(source, ref this._target);
}
/// <summary>
/// Converts a json deserialized object (simple type, dictionary or list) to a new instance of the specified target type.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="targetType">Type of the target.</param>
/// <param name="includeNonPublic">if set to <c>true</c> [include non public].</param>
/// <returns>The target object.</returns>
internal static Object FromJsonResult(Object source,
Type targetType,
Boolean includeNonPublic) {
Object nullRef = null;
return new Converter(source, targetType, ref nullRef, includeNonPublic).GetResult();
}
private static Object FromJsonResult(Object source,
Type targetType,
ref Object targetInstance,
Boolean includeNonPublic) => new Converter(source, targetType, ref targetInstance, includeNonPublic).GetResult();
private static Type GetAddMethodParameterType(Type targetType)
=> ListAddMethodCache.GetOrAdd(targetType,
x => x.GetMethods()
.FirstOrDefault(
m => m.Name.Equals(AddMethodName) && m.IsPublic && m.GetParameters().Length == 1)?
.GetParameters()[0]
.ParameterType);
private static void GetByteArray(String sourceString, ref Object target) {
try {
target = Convert.FromBase64String(sourceString);
} // Try conversion from Base 64
catch {
target = Encoding.UTF8.GetBytes(sourceString);
} // Get the string bytes in UTF8
}
private static Object GetSourcePropertyValue(IDictionary<String, Object> sourceProperties,
MemberInfo targetProperty) {
String targetPropertyName = MemberInfoNameCache.GetOrAdd(
targetProperty,
x => Runtime.AttributeCache.RetrieveOne<JsonPropertyAttribute>(x)?.PropertyName ?? x.Name);
return sourceProperties.GetValueOrDefault(targetPropertyName);
}
private Boolean TrySetInstance(Object targetInstance, Object source, ref Object target) {
if(targetInstance == null) {
// Try to create a default instance
try {
source.CreateTarget(this._targetType, this._includeNonPublic, ref target);
} catch {
return false;
}
} else {
target = targetInstance;
}
return true;
}
private Object GetResult() => this._target ?? this._targetType.GetDefault();
private void ResolveObject(Object source, ref Object target) {
switch(source) {
// Case 0: Special Cases Handling (Source and Target are of specific convertible types)
// Case 0.1: Source is string, Target is byte[]
case String sourceString when this._targetType == typeof(Byte[]):
GetByteArray(sourceString, ref target);
break;
// Case 1.1: Source is Dictionary, Target is IDictionary
case Dictionary<String, Object> sourceProperties when target is IDictionary targetDictionary:
this.PopulateDictionary(sourceProperties, targetDictionary);
break;
// Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type)
case Dictionary<String, Object> sourceProperties:
this.PopulateObject(sourceProperties);
break;
// Case 2.1: Source is List, Target is Array
case List<Object> sourceList when target is Array targetArray:
this.PopulateArray(sourceList, targetArray);
break;
// Case 2.2: Source is List, Target is IList
case List<Object> sourceList when target is IList targetList:
this.PopulateIList(sourceList, targetList);
break;
// Case 3: Source is a simple type; Attempt conversion
default:
String sourceStringValue = source.ToStringInvariant();
// Handle basic types or enumerations if not
if(!this._targetType.TryParseBasicType(sourceStringValue, out target)) {
this.GetEnumValue(sourceStringValue, ref target);
}
break;
}
}
private void PopulateIList(IList<Object> objects, IList list) {
Type parameterType = GetAddMethodParameterType(this._targetType);
if(parameterType == null) {
return;
}
foreach(Object item in objects) {
try {
_ = list.Add(FromJsonResult(
item,
parameterType,
this._includeNonPublic));
} catch {
// ignored
}
}
}
private void PopulateArray(IList<Object> objects, Array array) {
Type elementType = this._targetType.GetElementType();
for(Int32 i = 0; i < objects.Count; i++) {
try {
Object targetItem = FromJsonResult(
objects[i],
elementType,
this._includeNonPublic);
array.SetValue(targetItem, i);
} catch {
// ignored
}
}
}
private void GetEnumValue(String sourceStringValue, ref Object target) {
Type enumType = Nullable.GetUnderlyingType(this._targetType);
if(enumType == null && this._targetType.GetTypeInfo().IsEnum) {
enumType = this._targetType;
}
if(enumType == null) {
return;
}
try {
target = Enum.Parse(enumType, sourceStringValue);
} catch {
// ignored
}
}
private void PopulateDictionary(IDictionary<String, Object> sourceProperties, IDictionary targetDictionary) {
// find the add method of the target dictionary
MethodInfo addMethod = this._targetType.GetMethods()
.FirstOrDefault(
m => m.Name.Equals(AddMethodName) && m.IsPublic && m.GetParameters().Length == 2);
// skip if we don't have a compatible add method
if(addMethod == null) {
return;
}
ParameterInfo[] addMethodParameters = addMethod.GetParameters();
if(addMethodParameters[0].ParameterType != typeof(String)) {
return;
}
// Retrieve the target entry type
Type targetEntryType = addMethodParameters[1].ParameterType;
// Add the items to the target dictionary
foreach(KeyValuePair<String, Object> sourceProperty in sourceProperties) {
try {
Object targetEntryValue = FromJsonResult(
sourceProperty.Value,
targetEntryType,
this._includeNonPublic);
targetDictionary.Add(sourceProperty.Key, targetEntryValue);
} catch {
// ignored
}
}
}
private void PopulateObject(IDictionary<String, Object> sourceProperties) {
if(this._targetType.IsValueType()) {
this.PopulateFields(sourceProperties);
}
this.PopulateProperties(sourceProperties);
}
private void PopulateProperties(IDictionary<String, Object> sourceProperties) {
IEnumerable<PropertyInfo> properties = PropertyTypeCache.RetrieveFilteredProperties(this._targetType, false, p => p.CanWrite);
foreach(PropertyInfo property in properties) {
Object sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property);
if(sourcePropertyValue == null) {
continue;
}
try {
Object currentPropertyValue = !property.PropertyType.IsArray
? property.GetCacheGetMethod(this._includeNonPublic)(this._target)
: null;
Object targetPropertyValue = FromJsonResult(
sourcePropertyValue,
property.PropertyType,
ref currentPropertyValue,
this._includeNonPublic);
property.GetCacheSetMethod(this._includeNonPublic)(this._target, new[] { targetPropertyValue });
} catch {
// ignored
}
}
}
private void PopulateFields(IDictionary<String, Object> sourceProperties) {
foreach(FieldInfo field in FieldTypeCache.RetrieveAllFields(this._targetType)) {
Object sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field);
if(sourcePropertyValue == null) {
continue;
}
Object targetPropertyValue = FromJsonResult(
sourcePropertyValue,
field.FieldType,
this._includeNonPublic);
try {
field.SetValue(this._target, targetPropertyValue);
} catch {
// ignored
}
}
}
}
}
}