332 lines
12 KiB
C#
332 lines
12 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
|
|
namespace 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<global::System.Int32, global::System.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)) {
|
|
return;
|
|
}
|
|
|
|
this._options = options;
|
|
|
|
// 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._lastCommaSearch = FieldSeparatorChar + (this._options.Format ? Environment.NewLine : String.Empty);
|
|
this._builder = new StringBuilder();
|
|
|
|
this._result = obj switch
|
|
{
|
|
IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral,
|
|
IDictionary items => this.ResolveDictionary(items, depth),
|
|
IEnumerable enumerableZero when !enumerableZero.Cast<Object>().Any() => EmptyArrayLiteral,
|
|
IEnumerable enumerableBytes when enumerableBytes is Byte[] bytes => Serialize(bytes.ToBase64(), depth, this._options),
|
|
IEnumerable enumerable => this.ResolveEnumerable(enumerable, depth),
|
|
_ => this.ResolveObject(obj!, depth)
|
|
};
|
|
}
|
|
|
|
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.Value.ContainsKey(targetType)) {
|
|
return String.Empty;
|
|
}
|
|
|
|
String escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false);
|
|
|
|
return Decimal.TryParse(escapedValue, out _) ? $"{escapedValue}" : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
|
|
}
|
|
}
|
|
|
|
private static Boolean IsNonEmptyJsonArrayOrObject(String serialized) {
|
|
if(serialized == EmptyObjectLiteral || serialized == 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
|
|
global::System.Collections.Generic.Dictionary<global::System.String, global::System.Object?> objectDictionary = new Dictionary<global::System.String, global::System.Object?>();
|
|
|
|
if(String.IsNullOrWhiteSpace(this._options?.TypeSpecifier) == false) {
|
|
objectDictionary[this._options?.TypeSpecifier!] = targetType;
|
|
}
|
|
|
|
foreach(global::System.Collections.Generic.KeyValuePair<global::System.String, global::System.Reflection.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((Boolean)(this._options?.IncludeNonPublic)!)?.Invoke(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();
|
|
|
|
if(targetType.IsEnum) {
|
|
return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString();
|
|
}
|
|
|
|
global::System.Collections.Generic.Dictionary<global::System.String, global::System.Reflection.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
|
|
global::System.Collections.Generic.Dictionary<global::System.String, global::System.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
|
|
global::System.Collections.Generic.IEnumerable<global::System.Object> items = target.Cast<global::System.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
|
|
}
|
|
}
|
|
}
|