using System.Collections; using System.Collections.Concurrent; using System.Globalization; using System.Runtime.CompilerServices; using System.Text; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Toons; /// /// High-performance Toon (Token-Oriented Object Notation) serializer optimized for LLM readability. /// Features: /// - Human-readable indented format /// - Separate @meta/@types and @data sections for token efficiency /// - Type annotations and descriptions for better LLM understanding /// - Reference handling for circular/shared references /// - Optimized for Claude and other LLMs to easily parse and understand data structures /// public static partial class AcToonSerializer { /// /// Format version for Toon serialization. /// Incremented when breaking changes are made to format. /// public const string FormatVersion = "1.0"; #region Public API /// /// Serialize object to Toon format with default options. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string Serialize(T value) => Serialize(value, AcToonSerializerOptions.Default); /// /// Serialize object to Toon format with specified options. /// public static string Serialize(T value, AcToonSerializerOptions options) => Serialize(value, string.Empty, options); public static string Serialize(T value, string domainDescription, AcToonSerializerOptions options) { if (value == null) return "null"; var type = value.GetType(); // Handle primitive types directly without context if (TrySerializePrimitiveDirect(value, type, out var primitiveResult)) return primitiveResult; var context = ToonSerializationContextPool.Get(options); try { // Reference scanning if needed if (context.ReferenceHandling != ReferenceHandlingMode.None && !IsPrimitiveOrStringFast(type)) { ScanReferences(value, context, 0); } // Serialize based on mode switch (options.Mode) { case ToonSerializationMode.MetaOnly: WriteMetaSectionOnly(type, context, domainDescription); break; case ToonSerializationMode.DataOnly: WriteDataSectionOnly(value, type, context); break; case ToonSerializationMode.Full: default: WriteMetaSection(type, context, domainDescription); context.WriteLine(); WriteDataSection(value, type, context); break; } return context.GetResult(); } finally { ToonSerializationContextPool.Return(context); } } /// /// Serialize only type metadata (schema) for a given type. /// Useful for sending type information once at conversation start. /// public static string SerializeTypeMetadata() => SerializeTypeMetadata(typeof(T), string.Empty); public static string SerializeTypeMetadata(string domainDescription) => SerializeTypeMetadata(typeof(T), domainDescription); /// /// Serialize only type metadata (schema) for a given type. /// public static string SerializeTypeMetadata(Type type) => SerializeTypeMetadata(type, string.Empty); public static string SerializeTypeMetadata(Type type, string domainDescription) { var context = ToonSerializationContextPool.Get(AcToonSerializerOptions.MetaOnly); try { WriteMetaSectionOnly(type, context, domainDescription); return context.GetResult(); } finally { ToonSerializationContextPool.Return(context); } } /// /// Serialize metadata only for a collection of types (no data instances needed). /// Useful for documenting multiple types at once, or when you only have Type objects. /// /// Types to document /// Serialization options (optional, defaults to MetaOnly preset) /// Metadata-only Toon format string with @meta and @types sections public static string SerializeMetadata(IEnumerable types, AcToonSerializerOptions? options = null) => SerializeMetadata(types, string.Empty, options); public static string SerializeMetadata(IEnumerable types, string domainDescription, AcToonSerializerOptions? options = null) { // Return empty string if no types provided var typesList = types?.ToList(); if (typesList == null || typesList.Count == 0) { return string.Empty; } options ??= AcToonSerializerOptions.MetaOnly; var context = ToonSerializationContextPool.Get(options); try { WriteMetaSection(typesList, context, domainDescription); return context.GetResult(); } finally { ToonSerializationContextPool.Return(context); } } /// /// Serialize metadata only for multiple types (params array overload). /// /// Types to document /// Metadata-only Toon format string with @meta and @types sections public static string SerializeMetadata(params Type[] types) => SerializeMetadata((IEnumerable)types); public static string SerializeMetadata(string domainDescription, params Type[] types) => SerializeMetadata((IEnumerable)types, domainDescription); #endregion #region Primitive Serialization /// /// Fast path for primitive types that don't need context. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TrySerializePrimitiveDirect(object value, Type type, out string result) { var underlyingType = Nullable.GetUnderlyingType(type) ?? type; var typeCode = Type.GetTypeCode(underlyingType); switch (typeCode) { case TypeCode.String: result = EscapeString((string)value); return true; case TypeCode.Int32: result = ((int)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.Int64: result = ((long)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.Boolean: result = (bool)value ? "true" : "false"; return true; case TypeCode.Double: var d = (double)value; result = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture); return true; case TypeCode.Decimal: result = ((decimal)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.Single: var f = (float)value; result = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture); return true; case TypeCode.DateTime: result = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; case TypeCode.Byte: result = ((byte)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.Int16: result = ((short)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.UInt16: result = ((ushort)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.UInt32: result = ((uint)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.UInt64: result = ((ulong)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.SByte: result = ((sbyte)value).ToString(CultureInfo.InvariantCulture); return true; case TypeCode.Char: result = EscapeString(value.ToString()!); return true; } if (ReferenceEquals(underlyingType, GuidType)) { result = $"\"{((Guid)value).ToString("D")}\""; return true; } if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { result = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; } if (ReferenceEquals(underlyingType, TimeSpanType)) { result = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\""; return true; } if (underlyingType.IsEnum) { result = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); return true; } result = ""; return false; } /// /// Escape string for Toon format (double quotes, newlines, etc). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string EscapeString(string value) { if (string.IsNullOrEmpty(value)) return "\"\""; // Check if escaping is needed var needsEscaping = false; foreach (var c in value) { if (c == '"' || c == '\\' || c == '\n' || c == '\r' || c == '\t' || c < 32) { needsEscaping = true; break; } } if (!needsEscaping) return $"\"{value}\""; var sb = new StringBuilder(value.Length + 8); sb.Append('"'); foreach (var c in value) { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; default: if (c < 32) sb.Append($"\\u{((int)c):x4}"); else sb.Append(c); break; } } sb.Append('"'); return sb.ToString(); } #endregion #region Reference Scanning private static void ScanReferences(object? value, ToonSerializationContext context, int depth) { if (value == null || depth > context.MaxDepth) return; var type = value.GetType(); if (IsPrimitiveOrStringFast(type)) return; if (!context.TrackForScanning(value)) return; if (value is IDictionary dictionary) { foreach (DictionaryEntry entry in dictionary) { if (entry.Key != null) ScanReferences(entry.Key, context, depth + 1); if (entry.Value != null) ScanReferences(entry.Value, context, depth + 1); } return; } if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { foreach (var item in enumerable) { if (item != null) ScanReferences(item, context, depth + 1); } return; } var metadata = GetTypeMetadata(type); foreach (var prop in metadata.Properties) { var propValue = prop.GetValue(value); if (propValue != null) ScanReferences(propValue, context, depth + 1); } } #endregion #region Type Metadata // Temporary: own cache for static methods without context private static readonly ConcurrentDictionary MetadataCache = new(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ToonSerializeTypeMetadata GetTypeMetadata(Type type) => MetadataCache.GetOrAdd(type, static t => new ToonSerializeTypeMetadata(t)); #endregion }