#nullable enable using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace 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)) { 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().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 CreateDictionary(Dictionary fields, String targetType, Object target) { // Create the dictionary and extract the properties global::System.Collections.Generic.Dictionary objectDictionary = new Dictionary(); if(String.IsNullOrWhiteSpace(this._options?.TypeSpecifier) == false) { objectDictionary[this._options?.TypeSpecifier!] = targetType; } foreach(global::System.Collections.Generic.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((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 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 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 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 } } }