using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace Unosquare.Swan.Formatters { /// /// 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. /// public partial class Json { /// /// A simple JSON serializer. /// private class Serializer { #region Private Declarations private static readonly Dictionary IndentStrings = new Dictionary(); private readonly SerializerOptions _options; private readonly String _result; private readonly StringBuilder _builder; private readonly String _lastCommaSearch; #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// The object. /// The depth. /// The options. 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().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 CreateDictionary( Dictionary fields, String targetType, Object target) { // Create the dictionary and extract the properties Dictionary objectDictionary = new Dictionary(); if(String.IsNullOrWhiteSpace(this._options.TypeSpecifier) == false) { objectDictionary[this._options.TypeSpecifier] = targetType; } foreach(KeyValuePair 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 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 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 items = target.Cast(); 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))); } /// /// Removes the last comma in the current string builder. /// 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 } } }