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, int 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) _result = ResolveBasicType(obj); if (!string.IsNullOrWhiteSpace(_result)) return; _options = options; // Handle circular references correctly and avoid them if (options.IsObjectPresent(obj!)) { _result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}"; return; } // At this point, we will need to construct the object with a StringBuilder. _lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty); _builder = new StringBuilder(); _result = obj switch { IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral, IDictionary items => ResolveDictionary(items, depth), IEnumerable enumerableZero when !enumerableZero.Cast().Any() => EmptyArrayLiteral, IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options), IEnumerable enumerable => ResolveEnumerable(enumerable, depth), _ => ResolveObject(obj!, depth) }; } internal static string Serialize(object? obj, int 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 bool 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: var targetType = obj.GetType(); if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) return string.Empty; var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false); return decimal.TryParse(escapedValue, out _) ? $"{escapedValue}" : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; } } private static bool 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, bool quoted) { if (str == null) return string.Empty; var 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 (var 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 < ' ') { var escapeBytes = BitConverter.GetBytes((ushort)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 var objectDictionary = new Dictionary(); if (string.IsNullOrWhiteSpace(_options.TypeSpecifier) == false) objectDictionary[_options.TypeSpecifier] = targetType; foreach (var 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(_options.IncludeNonPublic)?.Invoke(target) : (field.Value as FieldInfo)?.GetValue(target); } catch { /* ignored */ } } return objectDictionary; } private string ResolveDictionary(IDictionary items, int depth) { Append(OpenObjectChar, depth); AppendLine(); // Iterate through the elements and output recursively var writeCount = 0; foreach (var key in items.Keys) { // Serialize and append the key (first char indented) Append(StringQuotedChar, depth + 1); Escape(key.ToString(), _builder); _builder .Append(StringQuotedChar) .Append(ValueSeparatorChar) .Append(" "); // Serialize and append the value var serializedValue = Serialize(items[key], depth + 1, _options); if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine(); Append(serializedValue, 0); // Add a comma and start a new line -- We will remove the last one when we are done writing the elements Append(FieldSeparatorChar, 0); AppendLine(); writeCount++; } // Output the end of the object and set the result RemoveLastComma(); Append(CloseObjectChar, writeCount > 0 ? depth : 0); return _builder.ToString(); } private string ResolveObject(object target, int depth) { var targetType = target.GetType(); if (targetType.IsEnum) return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString(); var fields = _options.GetProperties(targetType); if (fields.Count == 0 && string.IsNullOrWhiteSpace(_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 var objectDictionary = CreateDictionary(fields, targetType.ToString(), target); return Serialize(objectDictionary, depth, _options); } private string ResolveEnumerable(IEnumerable target, int depth) { // Cast the items as a generic object array var items = target.Cast(); Append(OpenArrayChar, depth); AppendLine(); // Iterate through the elements and output recursively var writeCount = 0; foreach (var entry in items) { var serializedValue = Serialize(entry, depth + 1, _options); if (IsNonEmptyJsonArrayOrObject(serializedValue)) Append(serializedValue, 0); else Append(serializedValue, depth + 1); Append(FieldSeparatorChar, 0); AppendLine(); writeCount++; } // Output the end of the array and set the result RemoveLastComma(); Append(CloseArrayChar, writeCount > 0 ? depth : 0); return _builder.ToString(); } private void SetIndent(int depth) { if (_options.Format == false || depth <= 0) return; _builder.Append(IndentStrings.GetOrAdd(depth, x => new string(' ', x * 4))); } /// /// Removes the last comma in the current string builder. /// private void RemoveLastComma() { if (_builder.Length < _lastCommaSearch.Length) return; if (_lastCommaSearch.Where((t, i) => _builder[_builder.Length - _lastCommaSearch.Length + i] != t).Any()) { return; } // If we got this far, we simply remove the comma character _builder.Remove(_builder.Length - _lastCommaSearch.Length, 1); } private void Append(string text, int depth) { SetIndent(depth); _builder.Append(text); } private void Append(char text, int depth) { SetIndent(depth); _builder.Append(text); } private void AppendLine() { if (_options.Format == false) return; _builder.Append(Environment.NewLine); } #endregion } } }