334 lines
12 KiB
C#
334 lines
12 KiB
C#
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;
|
|
|
|
/// <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 (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);
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
|
|
// Temporary: own cache for static methods without context
|
|
private static readonly ConcurrentDictionary<Type, ToonSerializeTypeMetadata> MetadataCache = new();
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static ToonSerializeTypeMetadata GetTypeMetadata(Type type)
|
|
=> MetadataCache.GetOrAdd(type, static t => new ToonSerializeTypeMetadata(t));
|
|
|
|
#endregion
|
|
}
|