AyCode.Core/AyCode.Core/Serializers/Toons/AcToonSerializer.cs

334 lines
12 KiB
C#

using System.Collections;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Toons;
/// <summary>
/// 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
/// </summary>
public static partial class AcToonSerializer
{
/// <summary>
/// Format version for Toon serialization.
/// Incremented when breaking changes are made to format.
/// </summary>
public const string FormatVersion = "1.0";
#region Public API
/// <summary>
/// Serialize object to Toon format with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Serialize<T>(T value) => Serialize(value, AcToonSerializerOptions.Default);
/// <summary>
/// Serialize object to Toon format with specified options.
/// </summary>
public static string Serialize<T>(T value, AcToonSerializerOptions options) => Serialize<T>(value, string.Empty, options);
public static string Serialize<T>(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 (options.UseReferenceHandling && !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);
}
}
/// <summary>
/// Serialize only type metadata (schema) for a given type.
/// Useful for sending type information once at conversation start.
/// </summary>
public static string SerializeTypeMetadata<T>() => SerializeTypeMetadata(typeof(T), string.Empty);
public static string SerializeTypeMetadata<T>(string domainDescription) => SerializeTypeMetadata(typeof(T), domainDescription);
/// <summary>
/// Serialize only type metadata (schema) for a given type.
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="types">Types to document</param>
/// <param name="options">Serialization options (optional, defaults to MetaOnly preset)</param>
/// <returns>Metadata-only Toon format string with @meta and @types sections</returns>
public static string SerializeMetadata(IEnumerable<Type> types, AcToonSerializerOptions? options = null) => SerializeMetadata(types, string.Empty, options);
public static string SerializeMetadata(IEnumerable<Type> 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);
}
}
/// <summary>
/// Serialize metadata only for multiple types (params array overload).
/// </summary>
/// <param name="types">Types to document</param>
/// <returns>Metadata-only Toon format string with @meta and @types sections</returns>
public static string SerializeMetadata(params Type[] types) => SerializeMetadata((IEnumerable<Type>)types);
public static string SerializeMetadata(string domainDescription, params Type[] types) => SerializeMetadata((IEnumerable<Type>)types, domainDescription);
#endregion
#region Primitive Serialization
/// <summary>
/// Fast path for primitive types that don't need context.
/// </summary>
[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;
}
/// <summary>
/// Escape string for Toon format (double quotes, newlines, etc).
/// </summary>
[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
/// <summary>
/// Gets or creates ToonTypeMetadata using TypeMetadataBase infrastructure.
/// This uses the shared GlobalMetadataCache and ThreadLocal cache for optimal performance.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ToonTypeMetadata GetTypeMetadata(Type type)
=> ToonTypeMetadata.GetOrCreateMetadata(type, static t => new ToonTypeMetadata(t));
#endregion
}