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

347 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
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 partial class Json {
/// <summary>
/// A simple JSON serializer.
/// </summary>
private class Serializer {
#region Private Declarations
private static readonly Dictionary<Int32, String> IndentStrings = new Dictionary<Int32, String>();
private readonly SerializerOptions _options;
private readonly String _result;
private readonly StringBuilder _builder;
private readonly String _lastCommaSearch;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Serializer" /> class.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="depth">The depth.</param>
/// <param name="options">The options.</param>
private Serializer(Object obj, Int32 depth, SerializerOptions options) {
if(depth > 20) {
throw new InvalidOperationException(
"The max depth (20) has been reached. Serializer can not continue.");
}
// Basic Type Handling (nulls, strings, number, date and bool)
this._result = ResolveBasicType(obj);
if(String.IsNullOrWhiteSpace(this._result) == false) {
return;
}
this._options = options;
this._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty);
// Handle circular references correctly and avoid them
if(options.IsObjectPresent(obj)) {
this._result = $"{{ \"$circref\": \"{Escape(obj.GetHashCode().ToStringInvariant(), false)}\" }}";
return;
}
// At this point, we will need to construct the object with a StringBuilder.
this._builder = new StringBuilder();
switch(obj) {
case IDictionary itemsZero when itemsZero.Count == 0:
this._result = EmptyObjectLiteral;
break;
case IDictionary items:
this._result = this.ResolveDictionary(items, depth);
break;
case IEnumerable enumerableZero when !enumerableZero.Cast<Object>().Any():
this._result = EmptyArrayLiteral;
break;
case IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes:
this._result = Serialize(bytes.ToBase64(), depth, this._options);
break;
case IEnumerable enumerable:
this._result = this.ResolveEnumerable(enumerable, depth);
break;
default:
this._result = this.ResolveObject(obj, depth);
break;
}
}
internal static String Serialize(Object obj, Int32 depth, SerializerOptions options) => new Serializer(obj, depth, options)._result;
#endregion
#region Helper Methods
private static String ResolveBasicType(Object obj) {
switch(obj) {
case null:
return NullLiteral;
case String s:
return Escape(s, true);
case Boolean b:
return b ? TrueLiteral : FalseLiteral;
case Type _:
case Assembly _:
case MethodInfo _:
case PropertyInfo _:
case EventInfo _:
return Escape(obj.ToString(), true);
case DateTime d:
return $"{StringQuotedChar}{d:s}{StringQuotedChar}";
default:
Type targetType = obj.GetType();
if(!Definitions.BasicTypesInfo.ContainsKey(targetType)) {
return String.Empty;
}
String escapedValue = Escape(Definitions.BasicTypesInfo[targetType].ToStringInvariant(obj), false);
return Decimal.TryParse(escapedValue, out _)
? $"{escapedValue}"
: $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
}
}
private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) {
if(serialized.Equals(EmptyObjectLiteral) || serialized.Equals(EmptyArrayLiteral)) {
return false;
}
// find the first position the character is not a space
return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault();
}
private static String Escape(String str, Boolean quoted) {
if(str == null) {
return String.Empty;
}
StringBuilder builder = new StringBuilder(str.Length * 2);
if(quoted) {
_ = builder.Append(StringQuotedChar);
}
Escape(str, builder);
if(quoted) {
_ = builder.Append(StringQuotedChar);
}
return builder.ToString();
}
private static void Escape(String str, StringBuilder builder) {
foreach(Char currentChar in str) {
switch(currentChar) {
case '\\':
case '"':
case '/':
_ = builder
.Append('\\')
.Append(currentChar);
break;
case '\b':
_ = builder.Append("\\b");
break;
case '\t':
_ = builder.Append("\\t");
break;
case '\n':
_ = builder.Append("\\n");
break;
case '\f':
_ = builder.Append("\\f");
break;
case '\r':
_ = builder.Append("\\r");
break;
default:
if(currentChar < ' ') {
Byte[] escapeBytes = BitConverter.GetBytes((UInt16)currentChar);
if(BitConverter.IsLittleEndian == false) {
Array.Reverse(escapeBytes);
}
_ = builder.Append("\\u")
.Append(escapeBytes[1].ToString("X").PadLeft(2, '0'))
.Append(escapeBytes[0].ToString("X").PadLeft(2, '0'));
} else {
_ = builder.Append(currentChar);
}
break;
}
}
}
private Dictionary<String, Object> CreateDictionary(
Dictionary<String, MemberInfo> fields,
String targetType,
Object target) {
// Create the dictionary and extract the properties
Dictionary<String, Object> objectDictionary = new Dictionary<String, Object>();
if(String.IsNullOrWhiteSpace(this._options.TypeSpecifier) == false) {
objectDictionary[this._options.TypeSpecifier] = targetType;
}
foreach(KeyValuePair<String, MemberInfo> field in fields) {
// Build the dictionary using property names and values
// Note: used to be: property.GetValue(target); but we would be reading private properties
try {
objectDictionary[field.Key] = field.Value is PropertyInfo property
? property.GetCacheGetMethod(this._options.IncludeNonPublic)(target)
: (field.Value as FieldInfo)?.GetValue(target);
} catch {
/* ignored */
}
}
return objectDictionary;
}
private String ResolveDictionary(IDictionary items, Int32 depth) {
this.Append(OpenObjectChar, depth);
this.AppendLine();
// Iterate through the elements and output recursively
Int32 writeCount = 0;
foreach(Object key in items.Keys) {
// Serialize and append the key (first char indented)
this.Append(StringQuotedChar, depth + 1);
Escape(key.ToString(), this._builder);
_ = this._builder
.Append(StringQuotedChar)
.Append(ValueSeparatorChar)
.Append(" ");
// Serialize and append the value
String serializedValue = Serialize(items[key], depth + 1, this._options);
if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
this.AppendLine();
}
this.Append(serializedValue, 0);
// Add a comma and start a new line -- We will remove the last one when we are done writing the elements
this.Append(FieldSeparatorChar, 0);
this.AppendLine();
writeCount++;
}
// Output the end of the object and set the result
this.RemoveLastComma();
this.Append(CloseObjectChar, writeCount > 0 ? depth : 0);
return this._builder.ToString();
}
private String ResolveObject(Object target, Int32 depth) {
Type targetType = target.GetType();
Dictionary<String, MemberInfo> fields = this._options.GetProperties(targetType);
if(fields.Count == 0 && String.IsNullOrWhiteSpace(this._options.TypeSpecifier)) {
return EmptyObjectLiteral;
}
// If we arrive here, then we convert the object into a
// dictionary of property names and values and call the serialization
// function again
Dictionary<String, Object> objectDictionary = this.CreateDictionary(fields, targetType.ToString(), target);
return Serialize(objectDictionary, depth, this._options);
}
private String ResolveEnumerable(IEnumerable target, Int32 depth) {
// Cast the items as a generic object array
IEnumerable<Object> items = target.Cast<Object>();
this.Append(OpenArrayChar, depth);
this.AppendLine();
// Iterate through the elements and output recursively
Int32 writeCount = 0;
foreach(Object entry in items) {
String serializedValue = Serialize(entry, depth + 1, this._options);
if(IsNonEmptyJsonArrayOrObject(serializedValue)) {
this.Append(serializedValue, 0);
} else {
this.Append(serializedValue, depth + 1);
}
this.Append(FieldSeparatorChar, 0);
this.AppendLine();
writeCount++;
}
// Output the end of the array and set the result
this.RemoveLastComma();
this.Append(CloseArrayChar, writeCount > 0 ? depth : 0);
return this._builder.ToString();
}
private void SetIndent(Int32 depth) {
if(this._options.Format == false || depth <= 0) {
return;
}
_ = this._builder.Append(IndentStrings.GetOrAdd(depth, x => new String(' ', x * 4)));
}
/// <summary>
/// Removes the last comma in the current string builder.
/// </summary>
private void RemoveLastComma() {
if(this._builder.Length < this._lastCommaSearch.Length) {
return;
}
if(this._lastCommaSearch.Where((t, i) => this._builder[this._builder.Length - this._lastCommaSearch.Length + i] != t).Any()) {
return;
}
// If we got this far, we simply remove the comma character
_ = this._builder.Remove(this._builder.Length - this._lastCommaSearch.Length, 1);
}
private void Append(String text, Int32 depth) {
this.SetIndent(depth);
_ = this._builder.Append(text);
}
private void Append(Char text, Int32 depth) {
this.SetIndent(depth);
_ = this._builder.Append(text);
}
private void AppendLine() {
if(this._options.Format == false) {
return;
}
_ = this._builder.Append(Environment.NewLine);
}
#endregion
}
}
}