Compare commits
5 Commits
9fad870960
...
6b7f4bf44f
| Author | SHA1 | Date |
|---|---|---|
|
|
6b7f4bf44f | |
|
|
d2caa2234d | |
|
|
bbb21dbb67 | |
|
|
028c80db94 | |
|
|
46c12bf5be |
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet build:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -377,3 +377,4 @@ FodyWeavers.xsd
|
|||
/CoverageReport
|
||||
/Test_Benchmark_Results
|
||||
/size_output.txt
|
||||
/reports
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
AyCode.Core.targets = AyCode.Core.targets
|
||||
RunQuickBenchmark.bat = RunQuickBenchmark.bat
|
||||
RunQuickBenchmark.ps1 = RunQuickBenchmark.ps1
|
||||
ToonExtendedInfo.txt = ToonExtendedInfo.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Benchmark", "AyCode.Benchmark\AyCode.Benchmark.csproj", "{A20861A9-411E-6150-BF5C-69E8196E5D22}"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers.Toons;
|
||||
using System.Buffers;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -11,6 +12,7 @@ namespace AyCode.Core.Compression;
|
|||
/// </summary>
|
||||
public static class BrotliHelper
|
||||
{
|
||||
//[ToonDescription("Unique identifier for the person")]
|
||||
private const int DefaultBufferSize = 4096;
|
||||
private const int MaxStackAllocSize = 1024;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AyCode.Core.Loggers;
|
||||
|
||||
/// <summary>
|
||||
/// ILoggerProvider implementation that creates logger instances using AcLoggerBase.
|
||||
/// Since AcLoggerBase now implements ILogger directly, this provider just wraps
|
||||
/// the base logger with category-specific instances.
|
||||
/// </summary>
|
||||
public sealed class AcLoggerProvider<TLogger> : ILoggerProvider where TLogger : AcLoggerBase
|
||||
{
|
||||
private readonly Func<string, TLogger> _loggerFactory;
|
||||
private readonly ConcurrentDictionary<string, TLogger> _loggers = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a provider that uses a factory function to create category-specific loggers.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Factory function that creates a logger for a given category name.</param>
|
||||
public AcLoggerProvider(Func<string, TLogger> loggerFactory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return _loggers.GetOrAdd(categoryName, _loggerFactory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loggers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering AcLogger with Microsoft's DI and logging infrastructure.
|
||||
/// </summary>
|
||||
public static class AcLoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds AcLogger as a logging provider using a factory function.
|
||||
/// The factory receives the category name and should return a configured logger instance.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// builder.Logging.AddAcLogger(categoryName => new MyLogger(categoryName));
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static ILoggingBuilder AddAcLogger<TLogger>(this ILoggingBuilder builder, Func<string, TLogger> loggerFactory)
|
||||
where TLogger : AcLoggerBase
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||
|
||||
builder.AddProvider(new AcLoggerProvider<TLogger>(loggerFactory));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears default providers and adds only AcLogger.
|
||||
/// Use this if you want ONLY your logger, not Microsoft's console/debug loggers.
|
||||
/// </summary>
|
||||
public static ILoggingBuilder UseOnlyAcLogger<TLogger>(this ILoggingBuilder builder, Func<string, TLogger> loggerFactory)
|
||||
where TLogger : AcLoggerBase
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||
|
||||
builder.ClearProviders();
|
||||
builder.AddProvider(new AcLoggerProvider<TLogger>(loggerFactory));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,10 @@ using System.Security.AccessControl;
|
|||
using AyCode.Core.Consts;
|
||||
using AyCode.Core.Enums;
|
||||
using AyCode.Utils.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
using AcLogLevel = AyCode.Core.Loggers.LogLevel;
|
||||
using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace AyCode.Core.Loggers;
|
||||
|
||||
|
|
@ -12,11 +15,18 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
{
|
||||
protected readonly List<IAcLogWriterBase> LogWriters = [];
|
||||
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Error;
|
||||
public AcLogLevel LogLevel { get; set; } = AcLogLevel.Error;
|
||||
public AppType AppType { get; set; } = AppType.Server;
|
||||
|
||||
public string? CategoryName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, long category names (e.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler")
|
||||
/// will be shortened to just the class name (e.g., "HubConnectionHandler").
|
||||
/// Default is true for consistency with typeof(T).Name usage.
|
||||
/// </summary>
|
||||
public bool ShortenCategoryNames { get; set; } = true;
|
||||
|
||||
protected AcLoggerBase() : this(null)
|
||||
{
|
||||
}
|
||||
|
|
@ -26,7 +36,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
CategoryName = categoryName ?? "...";
|
||||
|
||||
AppType = AcEnv.AppConfiguration.GetEnum<AppType>("AyCode:Logger:AppType");
|
||||
LogLevel = AcEnv.AppConfiguration.GetEnum<LogLevel>("AyCode:Logger:LogLevel");
|
||||
LogLevel = AcEnv.AppConfiguration.GetEnum<AcLogLevel>("AyCode:Logger:LogLevel");
|
||||
|
||||
foreach (var logWriterSection in AcEnv.AppConfiguration.GetSection("AyCode:Logger:LogWriters").GetChildren())
|
||||
{
|
||||
|
|
@ -41,7 +51,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
continue;
|
||||
}
|
||||
|
||||
var logWriterLogLevel = logWriterSection.GetEnum<LogLevel>("LogLevel");
|
||||
var logWriterLogLevel = logWriterSection.GetEnum<AcLogLevel>("LogLevel");
|
||||
|
||||
if (Activator.CreateInstance(logWriterType, AppType, logWriterLogLevel, CategoryName) is IAcLogWriterBase logWriter) LogWriters.Add(logWriter);
|
||||
else Console.Error.WriteLine($"{GetType().Name}; Can't create logWriterType instance; logWriterType: {logWriterType?.AssemblyQualifiedName};");
|
||||
|
|
@ -54,11 +64,11 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
}
|
||||
|
||||
protected AcLoggerBase(string? categoryName, params IAcLogWriterBase[] logWriters) :
|
||||
this(AcEnv.AppConfiguration.GetEnum<AppType>("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum<LogLevel>("AyCode:Logger:LogLevel"), categoryName, logWriters)
|
||||
this(AcEnv.AppConfiguration.GetEnum<AppType>("AyCode:Logger:AppType"), AcEnv.AppConfiguration.GetEnum<AcLogLevel>("AyCode:Logger:LogLevel"), categoryName, logWriters)
|
||||
{
|
||||
}
|
||||
|
||||
protected AcLoggerBase(AppType appType, LogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
|
||||
protected AcLoggerBase(AppType appType, AcLogLevel logLevel, string? categoryName, params IAcLogWriterBase[] logWriters)
|
||||
{
|
||||
AppType = appType;
|
||||
LogLevel = logLevel;
|
||||
|
|
@ -72,9 +82,11 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
public List<IAcLogWriterBase> GetWriters => [.. LogWriters];
|
||||
public TLogWriter? Writer<TLogWriter>() where TLogWriter : IAcLogWriterBase => LogWriters.OfType<TLogWriter>().FirstOrDefault();
|
||||
|
||||
#region IAcLogWriterBase Implementation
|
||||
|
||||
public virtual void Detail(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Detail) LogWriters.ForEach(x => x.Detail(text, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
|
@ -83,7 +95,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
public virtual void Debug(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Debug) LogWriters.ForEach(x => x.Debug(text, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
|
@ -92,7 +104,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
public virtual void Info(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Info) LogWriters.ForEach(x => x.Info(text, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
|
@ -101,7 +113,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
public virtual void Warning(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Warning) LogWriters.ForEach(x => x.Warning(text, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
|
@ -110,7 +122,7 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
public virtual void Suggest(string? text, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Suggest) LogWriters.ForEach(x => x.Suggest(text, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
|
|
@ -119,37 +131,36 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
public virtual void Error(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
{
|
||||
if (LogLevel <= LogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName));
|
||||
if (LogLevel <= AcLogLevel.Error) LogWriters.ForEach(x => x.Error(text, ex, categoryName ?? CategoryName, memberName));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void ErrorConditional(string? text, Exception? ex = null, string? categoryName = null, [CallerMemberName] string? memberName = null)
|
||||
=> Error(text, ex, categoryName, memberName);
|
||||
|
||||
public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
|
||||
public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
|
||||
=> Write(appType, logLevel, logText, callerMemberName, categoryName, null, null);
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
|
||||
public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName)
|
||||
=> Write(appType, logLevel, logText, callerMemberName, categoryName, null, null);
|
||||
|
||||
public void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
|
||||
public void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
|
||||
=> Write(appType, logLevel, logText, callerMemberName, categoryName, ex?.GetType().Name, ex?.ToString());
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
|
||||
public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, Exception? ex)
|
||||
=> Write(appType, logLevel, logText, callerMemberName, categoryName, ex);
|
||||
|
||||
public virtual void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
|
||||
public virtual void Write(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
|
||||
{
|
||||
if (LogLevel <= logLevel) LogWriters.ForEach(x => x.Write(appType, logLevel, logText, callerMemberName, categoryName ?? CategoryName, errorType, exMessage));
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void WriteConditional(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
|
||||
public void WriteConditional(AppType appType, AcLogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage)
|
||||
=> Write(appType, logLevel, logText, callerMemberName, categoryName, errorType, exMessage);
|
||||
|
||||
|
||||
public void Write(IAcLogItemClient logItem)
|
||||
{
|
||||
if (LogLevel <= logItem.LogLevel) LogWriters.ForEach(x => x.Write(logItem));
|
||||
|
|
@ -157,4 +168,114 @@ public abstract class AcLoggerBase : IAcLoggerBase
|
|||
|
||||
[Conditional("DEBUG")]
|
||||
public void WriteConditional(IAcLogItemClient logItem) => Write(logItem);
|
||||
|
||||
#endregion
|
||||
|
||||
#region ILogger Implementation
|
||||
|
||||
/// <summary>
|
||||
/// ILogger.BeginScope - AcLoggerBase doesn't support scopes, returns no-op disposable.
|
||||
/// </summary>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// ILogger.IsEnabled - Checks if the specified Microsoft log level is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled(MsLogLevel logLevel)
|
||||
{
|
||||
var acLogLevel = MapFromMsLogLevel(logLevel);
|
||||
return LogLevel <= acLogLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ILogger.Log - Main logging method called by Microsoft services.
|
||||
/// </summary>
|
||||
public void Log<TState>(MsLogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
var message = formatter(state, exception);
|
||||
|
||||
if (string.IsNullOrEmpty(message) && exception == null)
|
||||
return;
|
||||
|
||||
//var fullMessage = eventId.Id != 0
|
||||
// ? $"[{eventId.Id}:{eventId.Name}] {message}"
|
||||
// : message;
|
||||
|
||||
var fullMessage = message;
|
||||
var category = ShortenCategoryNames ? GetShortCategoryName(CategoryName) : CategoryName;
|
||||
|
||||
// Use eventId.Name as memberName if available, otherwise null (will show as empty, not "Log")
|
||||
var memberName = !string.IsNullOrEmpty(eventId.Name) ? $"{eventId.Name}:{eventId.Id}" : "Log";
|
||||
|
||||
switch (logLevel)
|
||||
{
|
||||
case MsLogLevel.Trace:
|
||||
Detail(fullMessage, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.Debug:
|
||||
Debug(fullMessage, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.Information:
|
||||
Info(fullMessage, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.Warning:
|
||||
Warning(fullMessage, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.Error:
|
||||
Error(fullMessage, exception, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.Critical:
|
||||
Error($"[CRITICAL] {fullMessage}", exception, category, memberName);
|
||||
break;
|
||||
case MsLogLevel.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortens a fully qualified type name to just the class name.
|
||||
/// E.g., "Microsoft.AspNetCore.SignalR.HubConnectionHandler" -> "HubConnectionHandler"
|
||||
/// </summary>
|
||||
private static string? GetShortCategoryName(string? categoryName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(categoryName))
|
||||
return categoryName;
|
||||
|
||||
var lastDot = categoryName.LastIndexOf('.');
|
||||
return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps Microsoft.Extensions.Logging.LogLevel to AyCode.Core.Loggers.LogLevel.
|
||||
/// </summary>
|
||||
private static AcLogLevel MapFromMsLogLevel(MsLogLevel msLogLevel)
|
||||
{
|
||||
return msLogLevel switch
|
||||
{
|
||||
MsLogLevel.Trace => AcLogLevel.Detail,
|
||||
MsLogLevel.Debug => AcLogLevel.Debug,
|
||||
MsLogLevel.Information => AcLogLevel.Info,
|
||||
MsLogLevel.Warning => AcLogLevel.Warning,
|
||||
MsLogLevel.Error => AcLogLevel.Error,
|
||||
MsLogLevel.Critical => AcLogLevel.Error,
|
||||
MsLogLevel.None => AcLogLevel.Disabled,
|
||||
_ => AcLogLevel.Info
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op scope implementation for ILogger.BeginScope.
|
||||
/// </summary>
|
||||
private sealed class NullScope : IDisposable
|
||||
{
|
||||
public static NullScope Instance { get; } = new();
|
||||
private NullScope() { }
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
namespace AyCode.Core.Loggers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
public interface IAcLoggerBase : IAcLogWriterBase
|
||||
namespace AyCode.Core.Loggers;
|
||||
|
||||
public interface IAcLoggerBase : IAcLogWriterBase, ILogger
|
||||
{
|
||||
public List<IAcLogWriterBase> GetWriters { get; }
|
||||
public TLogWriter? Writer<TLogWriter>() where TLogWriter : IAcLogWriterBase;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ public enum AcSerializerType : byte
|
|||
{
|
||||
Json = 0,
|
||||
Binary = 1,
|
||||
Toon = 2,
|
||||
}
|
||||
|
||||
public abstract class AcSerializerOptions
|
||||
|
|
|
|||
|
|
@ -0,0 +1,442 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract constraints from C# type system (value types, nullables, numeric ranges).
|
||||
/// </summary>
|
||||
private static string ExtractTypeConstraints(Type type)
|
||||
{
|
||||
var constraints = new List<string>();
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = underlyingType != null || !type.IsValueType;
|
||||
|
||||
// Nullable vs Required
|
||||
if (isNullable)
|
||||
constraints.Add("nullable");
|
||||
else
|
||||
constraints.Add("required");
|
||||
|
||||
var baseType = underlyingType ?? type;
|
||||
|
||||
// Numeric type ranges
|
||||
var typeCode = Type.GetTypeCode(baseType);
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Byte:
|
||||
constraints.Add("range: 0-255");
|
||||
break;
|
||||
case TypeCode.SByte:
|
||||
constraints.Add("range: -128-127");
|
||||
break;
|
||||
case TypeCode.Int16:
|
||||
constraints.Add("range: -32768-32767");
|
||||
break;
|
||||
case TypeCode.UInt16:
|
||||
constraints.Add("range: 0-65535");
|
||||
break;
|
||||
case TypeCode.Int32:
|
||||
constraints.Add("numeric");
|
||||
break;
|
||||
case TypeCode.UInt32:
|
||||
constraints.Add("range: 0-4294967295");
|
||||
break;
|
||||
case TypeCode.Int64:
|
||||
constraints.Add("numeric");
|
||||
break;
|
||||
case TypeCode.UInt64:
|
||||
constraints.Add("numeric, non-negative");
|
||||
break;
|
||||
case TypeCode.Single:
|
||||
case TypeCode.Double:
|
||||
case TypeCode.Decimal:
|
||||
constraints.Add("numeric");
|
||||
break;
|
||||
case TypeCode.Boolean:
|
||||
constraints.Add("boolean: true|false");
|
||||
break;
|
||||
}
|
||||
|
||||
// Enum values
|
||||
if (baseType.IsEnum)
|
||||
{
|
||||
var values = string.Join("|", Enum.GetNames(baseType).Take(5)); // Limit to 5 values
|
||||
var more = Enum.GetNames(baseType).Length > 5 ? "..." : "";
|
||||
constraints.Add($"enum: {values}{more}");
|
||||
}
|
||||
|
||||
return string.Join(", ", constraints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract constraints from Microsoft DataAnnotations attributes.
|
||||
/// </summary>
|
||||
private static string ExtractDataAnnotationConstraints(PropertyInfo prop)
|
||||
{
|
||||
var constraints = new List<string>();
|
||||
|
||||
// [Required]
|
||||
if (prop.GetCustomAttribute<RequiredAttribute>() != null)
|
||||
constraints.Add("required");
|
||||
|
||||
// [Range]
|
||||
var range = prop.GetCustomAttribute<RangeAttribute>();
|
||||
if (range != null)
|
||||
constraints.Add($"range: {range.Minimum}-{range.Maximum}");
|
||||
|
||||
// [MaxLength]
|
||||
var maxLen = prop.GetCustomAttribute<MaxLengthAttribute>();
|
||||
if (maxLen != null)
|
||||
constraints.Add($"max-length: {maxLen.Length}");
|
||||
|
||||
// [MinLength]
|
||||
var minLen = prop.GetCustomAttribute<MinLengthAttribute>();
|
||||
if (minLen != null)
|
||||
constraints.Add($"min-length: {minLen.Length}");
|
||||
|
||||
// [StringLength]
|
||||
var strLen = prop.GetCustomAttribute<StringLengthAttribute>();
|
||||
if (strLen != null)
|
||||
{
|
||||
if (strLen.MinimumLength > 0)
|
||||
constraints.Add($"length: {strLen.MinimumLength}-{strLen.MaximumLength}");
|
||||
else
|
||||
constraints.Add($"max-length: {strLen.MaximumLength}");
|
||||
}
|
||||
|
||||
// [EmailAddress]
|
||||
if (prop.GetCustomAttribute<EmailAddressAttribute>() != null)
|
||||
constraints.Add("email-format");
|
||||
|
||||
// [Phone]
|
||||
if (prop.GetCustomAttribute<PhoneAttribute>() != null)
|
||||
constraints.Add("phone-format");
|
||||
|
||||
// [Url]
|
||||
if (prop.GetCustomAttribute<UrlAttribute>() != null)
|
||||
constraints.Add("url-format");
|
||||
|
||||
// [CreditCard]
|
||||
if (prop.GetCustomAttribute<CreditCardAttribute>() != null)
|
||||
constraints.Add("credit-card-format");
|
||||
|
||||
// [RegularExpression]
|
||||
var regex = prop.GetCustomAttribute<RegularExpressionAttribute>();
|
||||
if (regex != null)
|
||||
constraints.Add($"pattern: {regex.Pattern}");
|
||||
|
||||
return string.Join(", ", constraints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge constraints with priority: custom > ms > inferred > type.
|
||||
/// Handles deduplication and cleanup.
|
||||
/// </summary>
|
||||
private static string MergeConstraints(
|
||||
string? typeConstraints,
|
||||
string? msConstraints,
|
||||
string? inferredConstraints,
|
||||
string? customConstraints)
|
||||
{
|
||||
var all = new HashSet<string>();
|
||||
|
||||
// Add in reverse priority order (lower priority first)
|
||||
AddConstraintsToSet(all, typeConstraints);
|
||||
AddConstraintsToSet(all, inferredConstraints);
|
||||
AddConstraintsToSet(all, msConstraints);
|
||||
AddConstraintsToSet(all, customConstraints);
|
||||
|
||||
return string.Join(", ", all.OrderBy(GetConstraintPriority));
|
||||
}
|
||||
|
||||
private static void AddConstraintsToSet(HashSet<string> set, string? constraints)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(constraints)) return;
|
||||
|
||||
foreach (var c in constraints.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)))
|
||||
{
|
||||
// Remove duplicates based on constraint type
|
||||
var constraintType = c.Split(':')[0].Trim();
|
||||
|
||||
// Remove existing constraints of same type (e.g., old range before adding new range)
|
||||
set.RemoveWhere(x => x.Split(':')[0].Trim().Equals(constraintType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
set.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetConstraintPriority(string constraint)
|
||||
{
|
||||
var lower = constraint.ToLowerInvariant();
|
||||
|
||||
// Priority order
|
||||
if (lower.StartsWith("required")) return 1;
|
||||
if (lower.StartsWith("nullable")) return 2;
|
||||
if (lower.StartsWith("range:")) return 3;
|
||||
if (lower.StartsWith("length:")) return 4;
|
||||
if (lower.StartsWith("max-length:")) return 5;
|
||||
if (lower.StartsWith("min-length:")) return 6;
|
||||
if (lower.Contains("-format")) return 7;
|
||||
if (lower.StartsWith("pattern:")) return 8;
|
||||
if (lower.StartsWith("enum:")) return 9;
|
||||
|
||||
return 100; // Other constraints last
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in description string.
|
||||
/// </summary>
|
||||
private static string ResolveDescriptionPlaceholders(
|
||||
string template,
|
||||
ToonPropertyAccessor prop,
|
||||
Type declaringType)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#Description] → Microsoft [Description]
|
||||
if (result.Contains("[#Description]"))
|
||||
{
|
||||
var msDesc = prop.PropertyInfo.GetCustomAttribute<DescriptionAttribute>();
|
||||
var value = msDesc?.Description ?? "";
|
||||
result = result.Replace("[#Description]", value);
|
||||
}
|
||||
|
||||
// [#DisplayName] → Microsoft [DisplayName]
|
||||
if (result.Contains("[#DisplayName]"))
|
||||
{
|
||||
var displayName = prop.PropertyInfo.GetCustomAttribute<DisplayNameAttribute>();
|
||||
var value = displayName?.DisplayName ?? prop.Name;
|
||||
result = result.Replace("[#DisplayName]", value);
|
||||
}
|
||||
|
||||
// [#SmartDescription] → Smart inference
|
||||
if (result.Contains("[#SmartDescription]"))
|
||||
{
|
||||
var value = GetPropertyDescription(declaringType, prop.Name, prop.PropertyType);
|
||||
result = result.Replace("[#SmartDescription]", value);
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in purpose string.
|
||||
/// </summary>
|
||||
private static string ResolvePurposePlaceholders(
|
||||
string template,
|
||||
ToonPropertyAccessor prop,
|
||||
Type declaringType)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#SmartPurpose] → Smart inference
|
||||
if (result.Contains("[#SmartPurpose]"))
|
||||
{
|
||||
var value = GetPropertyPurpose(declaringType, prop.Name);
|
||||
result = result.Replace("[#SmartPurpose]", value);
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in constraints string.
|
||||
/// </summary>
|
||||
private static string ResolveConstraintPlaceholders(
|
||||
string template,
|
||||
ToonPropertyAccessor prop)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#Range]
|
||||
if (result.Contains("[#Range]"))
|
||||
{
|
||||
var attr = prop.PropertyInfo.GetCustomAttribute<RangeAttribute>();
|
||||
var value = attr != null ? $"range: {attr.Minimum}-{attr.Maximum}" : "";
|
||||
result = result.Replace("[#Range]", value);
|
||||
}
|
||||
|
||||
// [#Required]
|
||||
if (result.Contains("[#Required]"))
|
||||
{
|
||||
var value = prop.PropertyInfo.GetCustomAttribute<RequiredAttribute>() != null ? "required" : "";
|
||||
result = result.Replace("[#Required]", value);
|
||||
}
|
||||
|
||||
// [#MaxLength]
|
||||
if (result.Contains("[#MaxLength]"))
|
||||
{
|
||||
var attr = prop.PropertyInfo.GetCustomAttribute<MaxLengthAttribute>();
|
||||
var value = attr != null ? $"max-length: {attr.Length}" : "";
|
||||
result = result.Replace("[#MaxLength]", value);
|
||||
}
|
||||
|
||||
// [#MinLength]
|
||||
if (result.Contains("[#MinLength]"))
|
||||
{
|
||||
var attr = prop.PropertyInfo.GetCustomAttribute<MinLengthAttribute>();
|
||||
var value = attr != null ? $"min-length: {attr.Length}" : "";
|
||||
result = result.Replace("[#MinLength]", value);
|
||||
}
|
||||
|
||||
// [#StringLength]
|
||||
if (result.Contains("[#StringLength]"))
|
||||
{
|
||||
var attr = prop.PropertyInfo.GetCustomAttribute<StringLengthAttribute>();
|
||||
string value = "";
|
||||
if (attr != null)
|
||||
{
|
||||
value = attr.MinimumLength > 0
|
||||
? $"length: {attr.MinimumLength}-{attr.MaximumLength}"
|
||||
: $"max-length: {attr.MaximumLength}";
|
||||
}
|
||||
result = result.Replace("[#StringLength]", value);
|
||||
}
|
||||
|
||||
// [#EmailAddress]
|
||||
if (result.Contains("[#EmailAddress]"))
|
||||
{
|
||||
var value = prop.PropertyInfo.GetCustomAttribute<EmailAddressAttribute>() != null ? "email-format" : "";
|
||||
result = result.Replace("[#EmailAddress]", value);
|
||||
}
|
||||
|
||||
// [#Phone]
|
||||
if (result.Contains("[#Phone]"))
|
||||
{
|
||||
var value = prop.PropertyInfo.GetCustomAttribute<PhoneAttribute>() != null ? "phone-format" : "";
|
||||
result = result.Replace("[#Phone]", value);
|
||||
}
|
||||
|
||||
// [#Url]
|
||||
if (result.Contains("[#Url]"))
|
||||
{
|
||||
var value = prop.PropertyInfo.GetCustomAttribute<UrlAttribute>() != null ? "url-format" : "";
|
||||
result = result.Replace("[#Url]", value);
|
||||
}
|
||||
|
||||
// [#CreditCard]
|
||||
if (result.Contains("[#CreditCard]"))
|
||||
{
|
||||
var value = prop.PropertyInfo.GetCustomAttribute<CreditCardAttribute>() != null ? "credit-card-format" : "";
|
||||
result = result.Replace("[#CreditCard]", value);
|
||||
}
|
||||
|
||||
// [#RegularExpression]
|
||||
if (result.Contains("[#RegularExpression]"))
|
||||
{
|
||||
var attr = prop.PropertyInfo.GetCustomAttribute<RegularExpressionAttribute>();
|
||||
var value = attr != null ? $"pattern: {attr.Pattern}" : "";
|
||||
result = result.Replace("[#RegularExpression]", value);
|
||||
}
|
||||
|
||||
// [#SmartTypeConstraints] → Type-derived constraints
|
||||
if (result.Contains("[#SmartTypeConstraints]"))
|
||||
{
|
||||
var value = ExtractTypeConstraints(prop.PropertyType);
|
||||
result = result.Replace("[#SmartTypeConstraints]", value);
|
||||
}
|
||||
|
||||
// [#SmartInferenceConstraints] → Smart inference
|
||||
if (result.Contains("[#SmartInferenceConstraints]"))
|
||||
{
|
||||
var value = GetInferredConstraints(prop.PropertyType, prop.Name);
|
||||
result = result.Replace("[#SmartInferenceConstraints]", value);
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in examples string.
|
||||
/// </summary>
|
||||
private static string ResolveExamplesPlaceholders(
|
||||
string template,
|
||||
ToonPropertyAccessor prop)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#GeneratedExample] → Generate example based on type
|
||||
if (result.Contains("[#GeneratedExample]"))
|
||||
{
|
||||
var value = GenerateExampleValue(prop.PropertyType);
|
||||
result = result.Replace("[#GeneratedExample]", value);
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up empty placeholders and formatting artifacts.
|
||||
/// </summary>
|
||||
private static string CleanupPlaceholders(string text)
|
||||
{
|
||||
// Remove unresolved placeholders
|
||||
text = Regex.Replace(text, @"\[#\w+\]", "");
|
||||
|
||||
// Remove double commas
|
||||
text = Regex.Replace(text, @",\s*,", ",");
|
||||
|
||||
// Remove leading/trailing commas
|
||||
text = Regex.Replace(text, @"^,\s*", "");
|
||||
text = Regex.Replace(text, @",\s*$", "");
|
||||
|
||||
// Remove multiple spaces
|
||||
text = Regex.Replace(text, @"\s{2,}", " ");
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate example value for a type.
|
||||
/// </summary>
|
||||
private static string GenerateExampleValue(Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => "42",
|
||||
TypeCode.Int64 => "1000",
|
||||
TypeCode.String => "example",
|
||||
TypeCode.Boolean => "true",
|
||||
TypeCode.DateTime => DateTime.Now.ToString("yyyy-MM-dd"),
|
||||
TypeCode.Decimal => "99.99",
|
||||
TypeCode.Double => "3.14",
|
||||
TypeCode.Single => "2.71",
|
||||
TypeCode.Byte => "255",
|
||||
_ => "value"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get inferred constraints based on property name patterns.
|
||||
/// </summary>
|
||||
private static string GetInferredConstraints(Type propertyType, string propertyName)
|
||||
{
|
||||
var constraints = new List<string>();
|
||||
var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
|
||||
// Email
|
||||
if (baseType == typeof(string) && propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("email-format");
|
||||
|
||||
// URL
|
||||
if (baseType == typeof(string) && propertyName.Contains("Url", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("url-format");
|
||||
|
||||
// Age
|
||||
if (IsIntegerType(baseType) && propertyName.Contains("Age", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("range: 0-150");
|
||||
|
||||
// Count (non-negative)
|
||||
if (IsIntegerType(baseType) && propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("non-negative");
|
||||
|
||||
return string.Join(", ", constraints);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Write data section only (for DataOnly mode).
|
||||
/// </summary>
|
||||
private static void WriteDataSectionOnly(object value, Type type, ToonSerializationContext context)
|
||||
{
|
||||
WriteDataSection(value, type, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write @data section.
|
||||
/// </summary>
|
||||
private static void WriteDataSection(object value, Type type, ToonSerializationContext context)
|
||||
{
|
||||
context.WriteLine("@data {");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
WriteValue(value, type, context, 0);
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteLine("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a value (dispatcher for different types).
|
||||
/// </summary>
|
||||
private static void WriteValue(object? value, Type type, ToonSerializationContext context, int depth)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
context.Write("null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try primitive first
|
||||
if (TryWritePrimitive(value, type, context))
|
||||
return;
|
||||
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
context.Write("null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for reference
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.Write($"@ref:{refId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dictionaries
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
WriteDictionary(dictionary, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle collections/arrays
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
WriteArray(enumerable, type, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle complex objects
|
||||
WriteObject(value, type, context, depth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write primitive value inline.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitive(object value, Type type, ToonSerializationContext context)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
string valueStr;
|
||||
string? typeHint = null;
|
||||
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.String:
|
||||
var strValue = (string)value;
|
||||
// Check if multi-line string format should be used
|
||||
if (context.Options.UseMultiLineStrings &&
|
||||
strValue.Length > context.Options.MultiLineStringThreshold)
|
||||
{
|
||||
context.Write(FormatMultiLineString(strValue, context));
|
||||
return true;
|
||||
}
|
||||
valueStr = EscapeString(strValue);
|
||||
typeHint = "string";
|
||||
break;
|
||||
case TypeCode.Int32:
|
||||
valueStr = ((int)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "int32";
|
||||
break;
|
||||
case TypeCode.Int64:
|
||||
valueStr = ((long)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "int64";
|
||||
break;
|
||||
case TypeCode.Boolean:
|
||||
valueStr = (bool)value ? "true" : "false";
|
||||
typeHint = "bool";
|
||||
break;
|
||||
case TypeCode.Double:
|
||||
var d = (double)value;
|
||||
if (double.IsNaN(d) || double.IsInfinity(d))
|
||||
{
|
||||
valueStr = "null";
|
||||
}
|
||||
else
|
||||
{
|
||||
valueStr = d.ToString("G17", CultureInfo.InvariantCulture);
|
||||
typeHint = "float64";
|
||||
}
|
||||
break;
|
||||
case TypeCode.Decimal:
|
||||
valueStr = ((decimal)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "decimal";
|
||||
break;
|
||||
case TypeCode.Single:
|
||||
var f = (float)value;
|
||||
if (float.IsNaN(f) || float.IsInfinity(f))
|
||||
{
|
||||
valueStr = "null";
|
||||
}
|
||||
else
|
||||
{
|
||||
valueStr = f.ToString("G9", CultureInfo.InvariantCulture);
|
||||
typeHint = "float32";
|
||||
}
|
||||
break;
|
||||
case TypeCode.DateTime:
|
||||
valueStr = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\"";
|
||||
typeHint = "datetime";
|
||||
break;
|
||||
case TypeCode.Byte:
|
||||
valueStr = ((byte)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "byte";
|
||||
break;
|
||||
case TypeCode.Int16:
|
||||
valueStr = ((short)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "int16";
|
||||
break;
|
||||
case TypeCode.UInt16:
|
||||
valueStr = ((ushort)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "uint16";
|
||||
break;
|
||||
case TypeCode.UInt32:
|
||||
valueStr = ((uint)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "uint32";
|
||||
break;
|
||||
case TypeCode.UInt64:
|
||||
valueStr = ((ulong)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "uint64";
|
||||
break;
|
||||
case TypeCode.SByte:
|
||||
valueStr = ((sbyte)value).ToString(CultureInfo.InvariantCulture);
|
||||
typeHint = "sbyte";
|
||||
break;
|
||||
case TypeCode.Char:
|
||||
valueStr = EscapeString(value.ToString()!);
|
||||
typeHint = "char";
|
||||
break;
|
||||
default:
|
||||
// Check special types
|
||||
if (ReferenceEquals(underlyingType, GuidType))
|
||||
{
|
||||
valueStr = $"\"{((Guid)value).ToString("D")}\"";
|
||||
typeHint = "guid";
|
||||
}
|
||||
else if (ReferenceEquals(underlyingType, DateTimeOffsetType))
|
||||
{
|
||||
valueStr = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\"";
|
||||
typeHint = "datetimeoffset";
|
||||
}
|
||||
else if (ReferenceEquals(underlyingType, TimeSpanType))
|
||||
{
|
||||
valueStr = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\"";
|
||||
typeHint = "timespan";
|
||||
}
|
||||
else if (underlyingType.IsEnum)
|
||||
{
|
||||
// Serialize enum as numeric value (not name) for token efficiency
|
||||
// The @types section contains the mapping
|
||||
var enumUnderlyingType = Enum.GetUnderlyingType(underlyingType);
|
||||
var numericValue = Convert.ChangeType(value, enumUnderlyingType);
|
||||
valueStr = Convert.ToString(numericValue, CultureInfo.InvariantCulture)!;
|
||||
typeHint = "enum";
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
context.Write(valueStr);
|
||||
|
||||
// Add inline type hint if enabled
|
||||
if (context.Options.UseInlineTypeHints && typeHint != null)
|
||||
{
|
||||
context.Write($" <{typeHint}>");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write complex object.
|
||||
/// </summary>
|
||||
private static void WriteObject(object value, Type type, ToonSerializationContext context, int depth)
|
||||
{
|
||||
var metadata = GetTypeMetadata(type);
|
||||
|
||||
// Write reference ID if this is a multi-referenced object
|
||||
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
|
||||
{
|
||||
context.Write($"@{refId} ");
|
||||
context.MarkAsWritten(value, refId);
|
||||
}
|
||||
|
||||
// Write type name if enabled
|
||||
if (context.Options.WriteTypeNames)
|
||||
{
|
||||
context.Write(metadata.ShortTypeName);
|
||||
}
|
||||
|
||||
context.WriteLine(" {");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
// Write properties
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
|
||||
// Skip null/default values if option is set
|
||||
if (context.Options.OmitDefaultValues && prop.IsDefaultValue(propValue))
|
||||
continue;
|
||||
|
||||
context.WriteIndent();
|
||||
context.Write(prop.Name);
|
||||
|
||||
// Token optimization: no spaces around '=' when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.Write(" = ");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Write('=');
|
||||
}
|
||||
|
||||
if (propValue == null)
|
||||
{
|
||||
context.WriteLine("null");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteValue(propValue, prop.PropertyType, context, nextDepth);
|
||||
context.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteIndent();
|
||||
context.Write("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write array/collection.
|
||||
/// </summary>
|
||||
private static void WriteArray(IEnumerable enumerable, Type type, ToonSerializationContext context, int depth)
|
||||
{
|
||||
var elementType = GetCollectionElementType(type) ?? typeof(object);
|
||||
|
||||
// Get count if possible
|
||||
var count = 0;
|
||||
if (enumerable is ICollection collection)
|
||||
{
|
||||
count = collection.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: enumerate to count
|
||||
foreach (var _ in enumerable) count++;
|
||||
}
|
||||
|
||||
// Write array header with optional type hint and count
|
||||
if (context.Options.ShowCollectionCount)
|
||||
{
|
||||
var elementTypeName = GetTypeDisplayName(elementType);
|
||||
context.Write($"<{elementTypeName}[]> (count: {count}) ");
|
||||
}
|
||||
|
||||
context.WriteLine("[");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
context.WriteIndent();
|
||||
WriteValue(item, item?.GetType() ?? elementType, context, nextDepth);
|
||||
context.WriteLine();
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteIndent();
|
||||
context.Write("]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get type display name for a type (used in array hints).
|
||||
/// </summary>
|
||||
private static string GetTypeDisplayName(Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.Double => "float64",
|
||||
TypeCode.Single => "float32",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.String => "string",
|
||||
TypeCode.DateTime => "datetime",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
_ => type.Name.ToLowerInvariant()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format multi-line string with triple-quote syntax.
|
||||
/// </summary>
|
||||
private static string FormatMultiLineString(string value, ToonSerializationContext context)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\"\"\"");
|
||||
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
sb.AppendLine();
|
||||
|
||||
// Split into lines and indent each
|
||||
var lines = value.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
for (var i = 0; i <= context.CurrentIndentLevel; i++)
|
||||
{
|
||||
sb.Append(context.Options.IndentString);
|
||||
}
|
||||
sb.AppendLine(line);
|
||||
}
|
||||
|
||||
// Closing quotes with proper indentation
|
||||
for (var i = 0; i < context.CurrentIndentLevel; i++)
|
||||
{
|
||||
sb.Append(context.Options.IndentString);
|
||||
}
|
||||
sb.Append("\"\"\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(value);
|
||||
sb.Append("\"\"\"");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write dictionary.
|
||||
/// </summary>
|
||||
private static void WriteDictionary(IDictionary dictionary, ToonSerializationContext context, int depth)
|
||||
{
|
||||
// Write dictionary header with count and type information
|
||||
if (context.Options.ShowCollectionCount)
|
||||
{
|
||||
var dictType = dictionary.GetType();
|
||||
var typeDisplayName = GetDictionaryTypeDisplayName(dictType);
|
||||
context.Write($"<{typeDisplayName}> (count: {dictionary.Count}) ");
|
||||
}
|
||||
|
||||
context.WriteLine("{");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
context.WriteIndent();
|
||||
|
||||
// Write key
|
||||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Key, keyType, context, nextDepth);
|
||||
|
||||
context.Write(" => ");
|
||||
|
||||
// Write value
|
||||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Value, valueType, context, nextDepth);
|
||||
|
||||
context.WriteLine();
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteIndent();
|
||||
context.Write("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get type display name for dictionary (e.g., "Dictionary<string, decimal>").
|
||||
/// </summary>
|
||||
private static string GetDictionaryTypeDisplayName(Type dictType)
|
||||
{
|
||||
if (IsDictionaryType(dictType, out var keyType, out var valueType))
|
||||
{
|
||||
var keyTypeName = GetSimpleTypeName(keyType);
|
||||
var valueTypeName = GetSimpleTypeName(valueType);
|
||||
return $"Dictionary<{keyTypeName}, {valueTypeName}>";
|
||||
}
|
||||
|
||||
return "dict";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get simple type name for dictionary type parameters.
|
||||
/// </summary>
|
||||
private static string GetSimpleTypeName(Type? type)
|
||||
{
|
||||
if (type == null) return "object";
|
||||
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = underlying != null;
|
||||
var actualType = underlying ?? type;
|
||||
|
||||
var baseName = Type.GetTypeCode(actualType) switch
|
||||
{
|
||||
TypeCode.Int32 => "int",
|
||||
TypeCode.Int64 => "long",
|
||||
TypeCode.Double => "double",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.Single => "float",
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.String => "string",
|
||||
TypeCode.DateTime => "DateTime",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.Int16 => "short",
|
||||
TypeCode.UInt16 => "ushort",
|
||||
TypeCode.UInt32 => "uint",
|
||||
TypeCode.UInt64 => "ulong",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
_ => actualType.Name
|
||||
};
|
||||
|
||||
return isNullable ? baseName + "?" : baseName;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,760 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Write meta section only (for MetaOnly mode).
|
||||
/// </summary>
|
||||
private static void WriteMetaSectionOnly(Type type, ToonSerializationContext context)
|
||||
{
|
||||
WriteMetaSection(type, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write @meta and @types sections for a single root type.
|
||||
/// </summary>
|
||||
private static void WriteMetaSection(Type type, ToonSerializationContext context)
|
||||
{
|
||||
if (!context.Options.UseMeta) return;
|
||||
|
||||
// Collect all types that need metadata
|
||||
var typesToDocument = new HashSet<Type>();
|
||||
CollectTypes(type, typesToDocument);
|
||||
|
||||
WriteMetaSectionCore(typesToDocument, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write @meta and @types sections for a collection of types.
|
||||
/// </summary>
|
||||
private static void WriteMetaSection(IEnumerable<Type> types, ToonSerializationContext context)
|
||||
{
|
||||
if (!context.Options.UseMeta) return;
|
||||
|
||||
// Collect all types that need metadata (including nested types)
|
||||
var typesToDocument = new HashSet<Type>();
|
||||
foreach (var type in types)
|
||||
{
|
||||
CollectTypes(type, typesToDocument);
|
||||
}
|
||||
|
||||
WriteMetaSectionCore(typesToDocument, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core logic for writing @meta and @types sections.
|
||||
/// </summary>
|
||||
private static void WriteMetaSectionCore(HashSet<Type> typesToDocument, ToonSerializationContext context)
|
||||
{
|
||||
// @meta header
|
||||
context.WriteLine("@meta {");
|
||||
context.CurrentIndentLevel++;
|
||||
context.WriteProperty("version", $"\"{FormatVersion}\"");
|
||||
context.WriteProperty("format", "\"toon\"");
|
||||
|
||||
// Write type list
|
||||
context.WriteIndent();
|
||||
context.Write("types");
|
||||
|
||||
// Token optimization: no spaces around '=' when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.Write(" = ");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Write('=');
|
||||
}
|
||||
|
||||
context.Write("[");
|
||||
var first = true;
|
||||
foreach (var t in typesToDocument)
|
||||
{
|
||||
if (!first) context.Write(", ");
|
||||
context.Write($"\"{t.Name}\"");
|
||||
first = false;
|
||||
}
|
||||
context.WriteLine("]");
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteLine("}");
|
||||
|
||||
// Token optimization: skip empty line when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.WriteLine();
|
||||
}
|
||||
|
||||
// @types section with descriptions
|
||||
context.WriteLine("@types {");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
foreach (var t in typesToDocument)
|
||||
{
|
||||
WriteTypeDefinition(t, context);
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.WriteLine("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all types that need documentation (recursive).
|
||||
/// </summary>
|
||||
private static void CollectTypes(Type type, HashSet<Type> types)
|
||||
{
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
if (!types.Add(underlyingType)) return; // Already processed
|
||||
|
||||
// Handle collections
|
||||
var elementType = GetCollectionElementType(underlyingType);
|
||||
if (elementType != null)
|
||||
{
|
||||
CollectTypes(elementType, types);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dictionaries
|
||||
if (IsDictionaryType(underlyingType, out var keyType, out var valueType))
|
||||
{
|
||||
if (keyType != null) CollectTypes(keyType, types);
|
||||
if (valueType != null) CollectTypes(valueType, types);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle object properties
|
||||
var metadata = GetTypeMetadata(underlyingType);
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
CollectTypes(prop.PropertyType, types);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write type definition with property descriptions.
|
||||
/// </summary>
|
||||
private static void WriteTypeDefinition(Type type, ToonSerializationContext context)
|
||||
{
|
||||
// Handle enum types specially
|
||||
if (type.IsEnum)
|
||||
{
|
||||
WriteEnumTypeDefinition(type, context);
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
|
||||
// Type description with fallback chain and placeholder resolution
|
||||
var typeDescription = GetFinalTypeDescription(type, metadata);
|
||||
context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\"");
|
||||
|
||||
// Type-level metadata (enhanced mode only)
|
||||
if (context.Options.UseEnhancedMetadata)
|
||||
{
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
// Table name
|
||||
var tableName = DetectTableName(type, metadata.CustomDescription);
|
||||
if (!string.IsNullOrEmpty(tableName))
|
||||
{
|
||||
context.WriteIndentedLine($"table-name: \"{tableName}\"");
|
||||
}
|
||||
|
||||
// Purpose
|
||||
var typePurpose = GetFinalTypePurpose(type, metadata);
|
||||
if (!string.IsNullOrEmpty(typePurpose))
|
||||
{
|
||||
context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
|
||||
if (metadata.Properties.Length == 0) return;
|
||||
|
||||
// Property descriptions
|
||||
context.CurrentIndentLevel++;
|
||||
foreach (var prop in metadata.Properties)
|
||||
{
|
||||
// Get final values with fallback chain + placeholder resolution
|
||||
var propDescription = GetFinalPropertyDescription(prop, type);
|
||||
var purpose = GetFinalPropertyPurpose(prop, type);
|
||||
var constraints = GetFinalPropertyConstraints(prop, type);
|
||||
var examples = GetFinalPropertyExamples(prop);
|
||||
|
||||
// Detect relationship metadata
|
||||
var relationshipMetadata = DetectRelationshipMetadata(prop.PropertyInfo, prop.CustomDescription);
|
||||
|
||||
var typeHint = prop.TypeDisplayName;
|
||||
|
||||
if (context.Options.UseEnhancedMetadata)
|
||||
{
|
||||
// Enhanced format with constraints and purpose
|
||||
context.WriteIndentedLine($"{prop.Name}: {typeHint}");
|
||||
context.CurrentIndentLevel++;
|
||||
context.WriteIndentedLine($"description: \"{propDescription}\"");
|
||||
if (!string.IsNullOrEmpty(purpose))
|
||||
context.WriteIndentedLine($"purpose: \"{purpose}\"");
|
||||
if (!string.IsNullOrEmpty(constraints))
|
||||
context.WriteIndentedLine($"constraints: \"{constraints}\"");
|
||||
if (!string.IsNullOrEmpty(examples))
|
||||
context.WriteIndentedLine($"examples: \"{examples}\"");
|
||||
|
||||
// Add relationship metadata
|
||||
if (relationshipMetadata.IsPrimaryKey)
|
||||
context.WriteIndentedLine($"primary-key: true");
|
||||
if (!string.IsNullOrEmpty(relationshipMetadata.ForeignKeyNavigationProperty))
|
||||
context.WriteIndentedLine($"foreign-key: \"{relationshipMetadata.ForeignKeyNavigationProperty}\"");
|
||||
if (relationshipMetadata.NavigationType.HasValue)
|
||||
{
|
||||
var navType = relationshipMetadata.NavigationType.Value.ToString();
|
||||
var navTypeFormatted = string.Concat(navType.Select((c, i) => i > 0 && char.IsUpper(c) ? "-" + char.ToLower(c) : char.ToLower(c).ToString()));
|
||||
context.WriteIndentedLine($"navigation: \"{navTypeFormatted}\"");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(relationshipMetadata.InverseProperty))
|
||||
context.WriteIndentedLine($"inverse-property: \"{relationshipMetadata.InverseProperty}\"");
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple format
|
||||
var relationshipHint = GetRelationshipHint(relationshipMetadata);
|
||||
var fullDescription = string.IsNullOrEmpty(relationshipHint)
|
||||
? propDescription
|
||||
: $"{propDescription} ({relationshipHint})";
|
||||
context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{fullDescription}\"");
|
||||
}
|
||||
}
|
||||
context.CurrentIndentLevel--;
|
||||
|
||||
// Token optimization: skip empty line when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write enum type definition with values and metadata.
|
||||
/// </summary>
|
||||
private static void WriteEnumTypeDefinition(Type enumType, ToonSerializationContext context)
|
||||
{
|
||||
var customDescription = enumType.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Type description with fallback
|
||||
var typeDescription = GetFinalEnumDescription(enumType, customDescription);
|
||||
context.WriteIndentedLine($"{enumType.Name}: enum");
|
||||
|
||||
if (context.Options.UseEnhancedMetadata)
|
||||
{
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrEmpty(typeDescription))
|
||||
{
|
||||
context.WriteIndentedLine($"description: \"{typeDescription}\"");
|
||||
}
|
||||
|
||||
// Purpose
|
||||
var typePurpose = customDescription?.Purpose;
|
||||
if (!string.IsNullOrEmpty(typePurpose))
|
||||
{
|
||||
context.WriteIndentedLine($"purpose: \"{typePurpose}\"");
|
||||
}
|
||||
|
||||
// Underlying type
|
||||
var underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
var underlyingTypeName = Type.GetTypeCode(underlyingType) switch
|
||||
{
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
_ => underlyingType.Name.ToLower()
|
||||
};
|
||||
context.WriteIndentedLine($"underlying-type: \"{underlyingTypeName}\"");
|
||||
|
||||
// Default value (first enum member)
|
||||
var enumValues = Enum.GetValues(enumType);
|
||||
if (enumValues.Length > 0)
|
||||
{
|
||||
var firstValue = enumValues.GetValue(0);
|
||||
var firstValueNumeric = Convert.ChangeType(firstValue, underlyingType);
|
||||
context.WriteIndentedLine($"default-value: {firstValueNumeric}");
|
||||
}
|
||||
|
||||
// Enum members with descriptions
|
||||
context.WriteIndentedLine("values:");
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
var names = Enum.GetNames(enumType);
|
||||
foreach (var name in names)
|
||||
{
|
||||
var field = enumType.GetField(name);
|
||||
var value = field?.GetValue(null);
|
||||
var numericValue = Convert.ChangeType(value, underlyingType);
|
||||
|
||||
// Get member description from ToonDescription attribute
|
||||
var memberDescription = field?.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
var description = memberDescription?.Description;
|
||||
|
||||
// Token optimization: format depends on indentation setting
|
||||
var separator = context.Options.UseIndentation ? " = " : "=";
|
||||
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
context.WriteIndentedLine($"{name}{separator}{numericValue}");
|
||||
context.CurrentIndentLevel++;
|
||||
context.WriteIndentedLine($"description: \"{description}\"");
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.WriteIndentedLine($"{name}{separator}{numericValue}");
|
||||
}
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple format - just show values
|
||||
var names = Enum.GetNames(enumType);
|
||||
var underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
context.CurrentIndentLevel++;
|
||||
|
||||
// Token optimization: format depends on indentation setting
|
||||
var separator = context.Options.UseIndentation ? " = " : "=";
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
var field = enumType.GetField(name);
|
||||
var value = field?.GetValue(null);
|
||||
var numericValue = Convert.ChangeType(value, underlyingType);
|
||||
context.WriteIndentedLine($"{name}{separator}{numericValue}");
|
||||
}
|
||||
|
||||
context.CurrentIndentLevel--;
|
||||
}
|
||||
|
||||
// Token optimization: skip empty line when indentation is disabled
|
||||
if (context.Options.UseIndentation)
|
||||
{
|
||||
context.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final enum description with fallback chain.
|
||||
/// Priority: ToonDescription.Description -> Microsoft [Description] -> Smart inference
|
||||
/// </summary>
|
||||
private static string GetFinalEnumDescription(Type enumType, ToonDescriptionAttribute? customDescription)
|
||||
{
|
||||
// 1. ToonDescription.Description
|
||||
if (!string.IsNullOrWhiteSpace(customDescription?.Description))
|
||||
{
|
||||
return customDescription.Description;
|
||||
}
|
||||
|
||||
// 2. Microsoft [Description] attribute
|
||||
var msDesc = enumType.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
|
||||
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
|
||||
{
|
||||
return msDesc.Description;
|
||||
}
|
||||
|
||||
// 3. Smart inference
|
||||
return $"Enum type with values: {string.Join(", ", Enum.GetNames(enumType))}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get description for a type (can be extended with XML comments or attributes).
|
||||
/// </summary>
|
||||
private static string GetTypeDescription(Type type)
|
||||
{
|
||||
// For now, generate simple descriptions
|
||||
// TODO: In the future, this could read XML documentation comments
|
||||
|
||||
if (type.IsEnum)
|
||||
return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}";
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
if (metadata.IsCollection)
|
||||
return $"Collection of {metadata.ElementType?.Name ?? "items"}";
|
||||
if (metadata.IsDictionary)
|
||||
return "Dictionary mapping keys to values";
|
||||
|
||||
return $"Object of type {type.Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get description for a property (can be extended with XML comments or attributes).
|
||||
/// </summary>
|
||||
private static string GetPropertyDescription(Type declaringType, string propertyName, Type propertyType)
|
||||
{
|
||||
// Enhanced description based on common patterns
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyType) != null || !propertyType.IsValueType;
|
||||
var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
|
||||
// Common property name patterns
|
||||
if (propertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
||||
return $"Unique identifier for {declaringType.Name}";
|
||||
if (propertyName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
||||
return $"Name of the {declaringType.Name}";
|
||||
if (propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
|
||||
return "Email address";
|
||||
if (propertyName.Contains("Phone", StringComparison.OrdinalIgnoreCase))
|
||||
return "Phone number";
|
||||
if (propertyName.Contains("Address", StringComparison.OrdinalIgnoreCase))
|
||||
return "Physical or mailing address";
|
||||
if (propertyName.Contains("Date", StringComparison.OrdinalIgnoreCase) || baseType == typeof(DateTime))
|
||||
return $"Date/time value for {propertyName}";
|
||||
if (propertyName.StartsWith("Is", StringComparison.OrdinalIgnoreCase) && baseType == typeof(bool))
|
||||
return $"Boolean flag indicating {propertyName.Substring(2)}";
|
||||
if (propertyName.StartsWith("Has", StringComparison.OrdinalIgnoreCase) && baseType == typeof(bool))
|
||||
return $"Boolean flag indicating possession of {propertyName.Substring(3)}";
|
||||
if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase) && IsIntegerType(baseType))
|
||||
return $"Count of {propertyName.Replace("Count", "")}";
|
||||
|
||||
// Collection types
|
||||
if (GetCollectionElementType(propertyType) != null)
|
||||
return $"Collection of {GetCollectionElementType(propertyType)?.Name ?? "items"} for {declaringType.Name}";
|
||||
|
||||
// Dictionary types
|
||||
if (IsDictionaryType(propertyType, out _, out _))
|
||||
return $"Dictionary mapping for {propertyName} in {declaringType.Name}";
|
||||
|
||||
// Default
|
||||
return $"Property {propertyName} of type {baseType.Name}{(isNullable ? " (nullable)" : "")}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get property constraints (nullable, required, etc.).
|
||||
/// </summary>
|
||||
private static string GetPropertyConstraints(Type propertyType, string propertyName)
|
||||
{
|
||||
var constraints = new List<string>();
|
||||
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyType) != null || !propertyType.IsValueType;
|
||||
if (isNullable)
|
||||
constraints.Add("nullable");
|
||||
else
|
||||
constraints.Add("required");
|
||||
|
||||
var baseType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
|
||||
// Type-specific constraints
|
||||
if (baseType == typeof(string))
|
||||
{
|
||||
if (propertyName.Contains("Email", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("email-format");
|
||||
if (propertyName.Contains("Url", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("url-format");
|
||||
}
|
||||
|
||||
if (IsIntegerType(baseType))
|
||||
{
|
||||
if (propertyName.Contains("Age", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("range: 0-150");
|
||||
else if (propertyName.EndsWith("Count", StringComparison.OrdinalIgnoreCase))
|
||||
constraints.Add("non-negative");
|
||||
}
|
||||
|
||||
return constraints.Count > 0 ? string.Join(", ", constraints) : "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get property purpose (what it's used for).
|
||||
/// </summary>
|
||||
private static string GetPropertyPurpose(Type declaringType, string propertyName)
|
||||
{
|
||||
// Common purposes based on property patterns
|
||||
if (propertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
||||
return "Primary key / unique identification";
|
||||
if (propertyName.Contains("CreatedAt", StringComparison.OrdinalIgnoreCase))
|
||||
return "Timestamp when entity was created";
|
||||
if (propertyName.Contains("UpdatedAt", StringComparison.OrdinalIgnoreCase) ||
|
||||
propertyName.Contains("ModifiedAt", StringComparison.OrdinalIgnoreCase))
|
||||
return "Timestamp of last update";
|
||||
if (propertyName.Contains("DeletedAt", StringComparison.OrdinalIgnoreCase))
|
||||
return "Soft delete timestamp";
|
||||
if (propertyName.StartsWith("Is", StringComparison.OrdinalIgnoreCase))
|
||||
return "Status flag";
|
||||
if (propertyName.Contains("Version", StringComparison.OrdinalIgnoreCase))
|
||||
return "Version tracking / concurrency control";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if type is an integer type.
|
||||
/// </summary>
|
||||
private static bool IsIntegerType(Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode is TypeCode.Byte or TypeCode.SByte or TypeCode.Int16 or
|
||||
TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
|
||||
TypeCode.Int64 or TypeCode.UInt64;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final property description with fallback chain and placeholder resolution.
|
||||
/// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference
|
||||
/// </summary>
|
||||
private static string GetFinalPropertyDescription(ToonPropertyAccessor prop, Type declaringType)
|
||||
{
|
||||
// 1. ToonDescription.Description (if not empty)
|
||||
var customDesc = prop.CustomDescription?.Description;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customDesc))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve it
|
||||
if (customDesc.Contains("[#"))
|
||||
{
|
||||
return ResolveDescriptionPlaceholders(customDesc, prop, declaringType);
|
||||
}
|
||||
// No placeholder → use as-is
|
||||
return customDesc;
|
||||
}
|
||||
|
||||
// 2. Microsoft [Description] attribute
|
||||
var msDesc = prop.PropertyInfo.GetCustomAttribute<DescriptionAttribute>();
|
||||
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
|
||||
return msDesc.Description;
|
||||
|
||||
// 3. Smart inference (fallback)
|
||||
return GetPropertyDescription(declaringType, prop.Name, prop.PropertyType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final property purpose with fallback chain and placeholder resolution.
|
||||
/// Priority: ToonDescription.Purpose (with placeholders) > Smart inference
|
||||
/// </summary>
|
||||
private static string GetFinalPropertyPurpose(ToonPropertyAccessor prop, Type declaringType)
|
||||
{
|
||||
var customPurpose = prop.CustomDescription?.Purpose;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customPurpose))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve it
|
||||
if (customPurpose.Contains("[#"))
|
||||
{
|
||||
return ResolvePurposePlaceholders(customPurpose, prop, declaringType);
|
||||
}
|
||||
return customPurpose;
|
||||
}
|
||||
|
||||
// Fallback: Smart inference
|
||||
return GetPropertyPurpose(declaringType, prop.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final property constraints with fallback chain and placeholder resolution.
|
||||
/// Priority: ToonDescription.Constraints (with placeholders merged) > Microsoft attributes > Type constraints > Smart inference
|
||||
/// </summary>
|
||||
private static string GetFinalPropertyConstraints(ToonPropertyAccessor prop, Type declaringType)
|
||||
{
|
||||
var customConstraints = prop.CustomDescription?.Constraints;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customConstraints))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve and merge
|
||||
if (customConstraints.Contains("[#"))
|
||||
{
|
||||
// Resolve placeholders first
|
||||
var resolved = ResolveConstraintPlaceholders(customConstraints, prop);
|
||||
|
||||
// Merge with type/smart constraints if resolved contains content
|
||||
if (!string.IsNullOrWhiteSpace(resolved))
|
||||
{
|
||||
var typeConstraints = ExtractTypeConstraints(prop.PropertyType);
|
||||
var inferredConstraints = GetInferredConstraints(prop.PropertyType, prop.Name);
|
||||
|
||||
return MergeConstraints(typeConstraints, null, inferredConstraints, resolved);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No placeholder → custom wins (replace mode)
|
||||
return customConstraints;
|
||||
}
|
||||
}
|
||||
|
||||
// No custom constraint → Fallback chain
|
||||
var msConstraints = ExtractDataAnnotationConstraints(prop.PropertyInfo);
|
||||
var typeConstraints2 = ExtractTypeConstraints(prop.PropertyType);
|
||||
var inferredConstraints2 = GetInferredConstraints(prop.PropertyType, prop.Name);
|
||||
|
||||
return MergeConstraints(typeConstraints2, msConstraints, inferredConstraints2, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final property examples with placeholder resolution.
|
||||
/// </summary>
|
||||
private static string GetFinalPropertyExamples(ToonPropertyAccessor prop)
|
||||
{
|
||||
var customExamples = prop.CustomDescription?.Examples;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customExamples))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve it
|
||||
if (customExamples.Contains("[#"))
|
||||
{
|
||||
return ResolveExamplesPlaceholders(customExamples, prop);
|
||||
}
|
||||
return customExamples;
|
||||
}
|
||||
|
||||
// No examples
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final type description with fallback chain and placeholder resolution.
|
||||
/// Priority: ToonDescription (with placeholders) > Microsoft [Description] > Smart inference
|
||||
/// </summary>
|
||||
private static string GetFinalTypeDescription(Type type, ToonTypeMetadata metadata)
|
||||
{
|
||||
// 1. ToonDescription.Description (if not empty)
|
||||
var customDesc = metadata.CustomDescription?.Description;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customDesc))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve it
|
||||
if (customDesc.Contains("[#"))
|
||||
{
|
||||
return ResolveTypeDescriptionPlaceholders(customDesc, type);
|
||||
}
|
||||
// No placeholder → use as-is
|
||||
return customDesc;
|
||||
}
|
||||
|
||||
// 2. Microsoft [Description] attribute
|
||||
var msDesc = type.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
|
||||
if (msDesc != null && !string.IsNullOrWhiteSpace(msDesc.Description))
|
||||
return msDesc.Description;
|
||||
|
||||
// 3. Smart inference (fallback)
|
||||
return GetTypeDescription(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in type description string.
|
||||
/// </summary>
|
||||
private static string ResolveTypeDescriptionPlaceholders(string template, Type type)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#Description] → Microsoft [Description]
|
||||
if (result.Contains("[#Description]"))
|
||||
{
|
||||
var msDesc = type.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
|
||||
var value = msDesc?.Description ?? "";
|
||||
result = result.Replace("[#Description]", value);
|
||||
}
|
||||
|
||||
// [#DisplayName] → Microsoft [DisplayName]
|
||||
if (result.Contains("[#DisplayName]"))
|
||||
{
|
||||
var displayName = type.GetCustomAttribute<System.ComponentModel.DisplayNameAttribute>();
|
||||
var value = displayName?.DisplayName ?? type.Name;
|
||||
result = result.Replace("[#DisplayName]", value);
|
||||
}
|
||||
|
||||
// [#SmartDescription] → Smart inference
|
||||
if (result.Contains("[#SmartDescription]"))
|
||||
{
|
||||
var value = GetTypeDescription(type);
|
||||
result = result.Replace("[#SmartDescription]", value);
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get final type purpose with fallback chain and placeholder resolution.
|
||||
/// Priority: ToonDescription.Purpose (with placeholders) > Smart inference (empty for classes)
|
||||
/// </summary>
|
||||
private static string GetFinalTypePurpose(Type type, ToonTypeMetadata metadata)
|
||||
{
|
||||
var customPurpose = metadata.CustomDescription?.Purpose;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customPurpose))
|
||||
{
|
||||
// Has [#...] placeholder? → Resolve it
|
||||
if (customPurpose.Contains("[#"))
|
||||
{
|
||||
return ResolveTypePurposePlaceholders(customPurpose, type);
|
||||
}
|
||||
return customPurpose;
|
||||
}
|
||||
|
||||
// No smart inference for class-level purpose - return empty
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve [#...] placeholders in type purpose string.
|
||||
/// </summary>
|
||||
private static string ResolveTypePurposePlaceholders(string template, Type type)
|
||||
{
|
||||
var result = template;
|
||||
|
||||
// [#SmartPurpose] → Would be empty for classes, so just remove it
|
||||
if (result.Contains("[#SmartPurpose]"))
|
||||
{
|
||||
result = result.Replace("[#SmartPurpose]", "");
|
||||
}
|
||||
|
||||
return CleanupPlaceholders(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get relationship hint string for simple format output.
|
||||
/// </summary>
|
||||
private static string GetRelationshipHint(RelationshipMetadata metadata)
|
||||
{
|
||||
var hints = new List<string>();
|
||||
|
||||
if (metadata.IsPrimaryKey)
|
||||
hints.Add("primary-key");
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.ForeignKeyNavigationProperty))
|
||||
hints.Add($"fk->{metadata.ForeignKeyNavigationProperty}");
|
||||
|
||||
if (metadata.NavigationType.HasValue)
|
||||
{
|
||||
var navType = metadata.NavigationType.Value switch
|
||||
{
|
||||
ToonRelationType.ManyToOne => "many-to-one",
|
||||
ToonRelationType.OneToMany => "one-to-many",
|
||||
ToonRelationType.OneToOne => "one-to-one",
|
||||
ToonRelationType.ManyToMany => "many-to-many",
|
||||
_ => null
|
||||
};
|
||||
if (navType != null)
|
||||
hints.Add(navType);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.InverseProperty))
|
||||
hints.Add($"inverse:{metadata.InverseProperty}");
|
||||
|
||||
return hints.Count > 0 ? string.Join(", ", hints) : string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Relationship metadata for a property.
|
||||
/// </summary>
|
||||
private sealed class RelationshipMetadata
|
||||
{
|
||||
public bool IsPrimaryKey { get; set; }
|
||||
public string? ForeignKeyNavigationProperty { get; set; }
|
||||
public ToonRelationType? NavigationType { get; set; }
|
||||
public string? InverseProperty { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects relationship metadata for a property with fallback chain.
|
||||
/// Priority: ToonDescription -> EF Core/Linq2Db attributes -> Convention-based detection.
|
||||
/// </summary>
|
||||
private static RelationshipMetadata DetectRelationshipMetadata(PropertyInfo property, ToonDescriptionAttribute? customDescription)
|
||||
{
|
||||
var metadata = new RelationshipMetadata();
|
||||
|
||||
// 1. IsPrimaryKey - Priority: ToonDescription -> EF Core [Key] -> Linq2Db [PrimaryKey] -> Convention
|
||||
if (customDescription?.IsPrimaryKey.HasValue == true)
|
||||
{
|
||||
metadata.IsPrimaryKey = customDescription.IsPrimaryKey.Value;
|
||||
}
|
||||
else if (TryGetEFCoreKey(property) || TryGetLinq2DbPrimaryKey(property))
|
||||
{
|
||||
metadata.IsPrimaryKey = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.IsPrimaryKey = IsConventionPrimaryKey(property);
|
||||
}
|
||||
|
||||
// 2. ForeignKey - Priority: ToonDescription -> EF Core [ForeignKey] -> Linq2Db [Association] -> Convention
|
||||
if (!string.IsNullOrEmpty(customDescription?.ForeignKey))
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = customDescription.ForeignKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCoreFk = TryGetEFCoreForeignKey(property);
|
||||
if (!string.IsNullOrEmpty(efCoreFk))
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = efCoreFk;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.ForeignKeyNavigationProperty = DetectConventionForeignKey(property);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Navigation - Priority: ToonDescription -> EF Core [InverseProperty] -> Linq2Db [Association] -> Convention
|
||||
if (customDescription?.Navigation.HasValue == true)
|
||||
{
|
||||
metadata.NavigationType = customDescription.Navigation.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var linq2dbNav = TryGetLinq2DbNavigation(property);
|
||||
if (linq2dbNav.HasValue)
|
||||
{
|
||||
metadata.NavigationType = linq2dbNav.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.NavigationType = DetectConventionNavigation(property);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. InverseProperty - Priority: ToonDescription -> EF Core [InverseProperty]
|
||||
if (!string.IsNullOrEmpty(customDescription?.InverseProperty))
|
||||
{
|
||||
metadata.InverseProperty = customDescription.InverseProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var efCoreInverse = TryGetEFCoreInverseProperty(property);
|
||||
if (!string.IsNullOrEmpty(efCoreInverse))
|
||||
{
|
||||
metadata.InverseProperty = efCoreInverse;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convention: Property named "Id" or "{TypeName}Id" is a primary key.
|
||||
/// </summary>
|
||||
private static bool IsConventionPrimaryKey(PropertyInfo property)
|
||||
{
|
||||
if (property.PropertyType != typeof(int) &&
|
||||
property.PropertyType != typeof(long) &&
|
||||
property.PropertyType != typeof(Guid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return property.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) ||
|
||||
property.Name.Equals(property.DeclaringType?.Name + "Id", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convention: Property named "{TypeName}Id" with a corresponding "{TypeName}" navigation property is a foreign key.
|
||||
/// Type-based validation: checks that navigation property is a complex type (not primitive/string).
|
||||
/// Example: "CompanyId" + "Company" property exists and is complex type -> foreign key to "Company".
|
||||
/// </summary>
|
||||
private static string? DetectConventionForeignKey(PropertyInfo property)
|
||||
{
|
||||
// Skip non-value types (we don't check specific FK type, could be int/long/Guid/etc)
|
||||
if (!property.PropertyType.IsValueType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if property name ends with "Id"
|
||||
if (!property.Name.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract potential navigation property name
|
||||
var navigationPropertyName = property.Name.Substring(0, property.Name.Length - 2);
|
||||
|
||||
// Check if corresponding navigation property exists
|
||||
var declaringType = property.DeclaringType;
|
||||
if (declaringType == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var navigationProperty = declaringType.GetProperty(navigationPropertyName, BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Type-based validation: navigation property must exist and be a complex type (not primitive/string)
|
||||
if (navigationProperty == null || IsPrimitiveOrStringFast(navigationProperty.PropertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Additional validation: navigation property type should have a primary key (IsPrimaryKey or Id property)
|
||||
if (!HasPrimaryKeyProperty(navigationProperty.PropertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return navigationPropertyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convention: Detect navigation type based on property type.
|
||||
/// Type-based validation with FK lookup:
|
||||
/// - ICollection<T> or List<T> -> OneToMany (if T has primary key)
|
||||
/// - Complex object type -> ManyToOne (if has corresponding FK property and has primary key)
|
||||
/// </summary>
|
||||
private static ToonRelationType? DetectConventionNavigation(PropertyInfo property)
|
||||
{
|
||||
var propertyType = property.PropertyType;
|
||||
|
||||
// Skip primitive types and strings
|
||||
if (IsPrimitiveOrStringFast(propertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for collection types (OneToMany)
|
||||
if (typeof(IEnumerable).IsAssignableFrom(propertyType) && propertyType != typeof(string))
|
||||
{
|
||||
// Extract element type from collection
|
||||
var elementType = GetCollectionElementType(propertyType);
|
||||
if (elementType != null && HasPrimaryKeyProperty(elementType))
|
||||
{
|
||||
// It's a collection of entities -> OneToMany
|
||||
return ToonRelationType.OneToMany;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Complex object type -> check if it's a navigation property
|
||||
// Type-based validation: must have a primary key
|
||||
if (!HasPrimaryKeyProperty(propertyType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look for a corresponding foreign key property to confirm it's a navigation
|
||||
var declaringType = property.DeclaringType;
|
||||
if (declaringType != null)
|
||||
{
|
||||
var foreignKeyPropertyName = property.Name + "Id";
|
||||
var foreignKeyProperty = declaringType.GetProperty(foreignKeyPropertyName, BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// Type-based validation: FK must be a value type
|
||||
if (foreignKeyProperty != null && foreignKeyProperty.PropertyType.IsValueType)
|
||||
{
|
||||
// Found corresponding foreign key -> ManyToOne
|
||||
return ToonRelationType.ManyToOne;
|
||||
}
|
||||
}
|
||||
|
||||
// No corresponding foreign key found
|
||||
// Return null to avoid false positives
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a type has a primary key property.
|
||||
/// First checks for IsPrimaryKey=true in ToonDescription, then falls back to "Id" property.
|
||||
/// </summary>
|
||||
private static bool HasPrimaryKeyProperty(Type type)
|
||||
{
|
||||
if (IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
// First: check for ToonDescription with IsPrimaryKey = true
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var customDescription = prop.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
if (customDescription?.IsPrimaryKey == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check for "Id" or "{TypeName}Id" property
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) ||
|
||||
prop.Name.Equals(type.Name + "Id", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Must be a value type (int, long, Guid, etc.)
|
||||
if (prop.PropertyType.IsValueType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#region EF Core Attribute Detection (reflection-based, no dependency)
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [Key] attribute via reflection (no EF Core dependency).
|
||||
/// </summary>
|
||||
private static bool TryGetEFCoreKey(PropertyInfo property)
|
||||
{
|
||||
var keyAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "KeyAttribute");
|
||||
return keyAttr != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [ForeignKey("NavigationPropertyName")] attribute via reflection.
|
||||
/// </summary>
|
||||
private static string? TryGetEFCoreForeignKey(PropertyInfo property)
|
||||
{
|
||||
var fkAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "ForeignKeyAttribute");
|
||||
|
||||
if (fkAttr != null)
|
||||
{
|
||||
// Get the Name property value (navigation property name)
|
||||
var nameProp = fkAttr.GetType().GetProperty("Name");
|
||||
return nameProp?.GetValue(fkAttr) as string;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [InverseProperty("PropertyName")] attribute via reflection.
|
||||
/// </summary>
|
||||
private static string? TryGetEFCoreInverseProperty(PropertyInfo property)
|
||||
{
|
||||
var inversePropAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "InversePropertyAttribute");
|
||||
|
||||
if (inversePropAttr != null)
|
||||
{
|
||||
var propertyProp = inversePropAttr.GetType().GetProperty("Property");
|
||||
return propertyProp?.GetValue(inversePropAttr) as string;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Linq2Db Attribute Detection (reflection-based, no dependency)
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [PrimaryKey] attribute via reflection (no Linq2Db dependency).
|
||||
/// </summary>
|
||||
private static bool TryGetLinq2DbPrimaryKey(PropertyInfo property)
|
||||
{
|
||||
var pkAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "PrimaryKeyAttribute");
|
||||
return pkAttr != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [Association] attribute and determine navigation type.
|
||||
/// Supports simple ThisKey/OtherKey associations. Expression-based associations are skipped.
|
||||
/// </summary>
|
||||
private static ToonRelationType? TryGetLinq2DbNavigation(PropertyInfo property)
|
||||
{
|
||||
var assocAttr = property.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "AssociationAttribute");
|
||||
|
||||
if (assocAttr == null)
|
||||
return null;
|
||||
|
||||
// Check if it's expression-based (has QueryExpressionMethod property set)
|
||||
var queryExprMethod = assocAttr.GetType().GetProperty("QueryExpressionMethod")?.GetValue(assocAttr) as string;
|
||||
if (!string.IsNullOrEmpty(queryExprMethod))
|
||||
{
|
||||
// Expression-based many-to-many - too complex, skip and fallback to convention
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simple association with ThisKey/OtherKey
|
||||
var thisKey = assocAttr.GetType().GetProperty("ThisKey")?.GetValue(assocAttr) as string;
|
||||
var otherKey = assocAttr.GetType().GetProperty("OtherKey")?.GetValue(assocAttr) as string;
|
||||
|
||||
var propertyType = property.PropertyType;
|
||||
|
||||
// Check if it's a collection (OneToMany or ManyToMany)
|
||||
if (typeof(IEnumerable).IsAssignableFrom(propertyType) && propertyType != typeof(string))
|
||||
{
|
||||
// Collection with ThisKey + OtherKey -> could be ManyToMany or OneToMany
|
||||
// If both ThisKey and OtherKey are set -> likely ManyToMany
|
||||
// If only ThisKey -> OneToMany
|
||||
if (!string.IsNullOrEmpty(thisKey) && !string.IsNullOrEmpty(otherKey))
|
||||
{
|
||||
// Could be ManyToMany, but without junction table info, treat as OneToMany
|
||||
return ToonRelationType.OneToMany;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ToonRelationType.OneToMany;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single object -> ManyToOne or OneToOne
|
||||
// Typically ManyToOne
|
||||
return ToonRelationType.ManyToOne;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TableName Detection (class-level)
|
||||
|
||||
/// <summary>
|
||||
/// Detect table name for a type with fallback chain.
|
||||
/// Priority: ToonDescription.TableName -> EF Core [Table] -> Linq2Db [Table] -> Convention (class name).
|
||||
/// </summary>
|
||||
internal static string? DetectTableName(Type type, ToonDescriptionAttribute? customDescription)
|
||||
{
|
||||
// 1. ToonDescription.TableName (explicit override)
|
||||
if (!string.IsNullOrEmpty(customDescription?.TableName))
|
||||
{
|
||||
return customDescription.TableName;
|
||||
}
|
||||
|
||||
// 2. EF Core [Table("name")] attribute (primary)
|
||||
var efCoreTable = TryGetEFCoreTableName(type);
|
||||
if (!string.IsNullOrEmpty(efCoreTable))
|
||||
{
|
||||
return efCoreTable;
|
||||
}
|
||||
|
||||
// 3. Linq2Db [Table(Name = "name")] attribute (if EF Core not found)
|
||||
var linq2dbTable = TryGetLinq2DbTableName(type);
|
||||
if (!string.IsNullOrEmpty(linq2dbTable))
|
||||
{
|
||||
return linq2dbTable;
|
||||
}
|
||||
|
||||
// 4. Convention: class name (fallback)
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect EF Core [Table("name")] or [Table(Name = "name", Schema = "schema")] attribute via reflection.
|
||||
/// </summary>
|
||||
private static string? TryGetEFCoreTableName(Type type)
|
||||
{
|
||||
var tableAttr = type.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "TableAttribute");
|
||||
|
||||
if (tableAttr != null)
|
||||
{
|
||||
// EF Core TableAttribute has "Name" property
|
||||
var nameProp = tableAttr.GetType().GetProperty("Name");
|
||||
return nameProp?.GetValue(tableAttr) as string;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect Linq2Db [Table(Name = "name")] attribute via reflection.
|
||||
/// </summary>
|
||||
private static string? TryGetLinq2DbTableName(Type type)
|
||||
{
|
||||
var tableAttr = type.GetCustomAttributes()
|
||||
.FirstOrDefault(a => a.GetType().Name == "TableAttribute");
|
||||
|
||||
if (tableAttr != null)
|
||||
{
|
||||
// Linq2Db TableAttribute also has "Name" property
|
||||
var nameProp = tableAttr.GetType().GetProperty("Name");
|
||||
return nameProp?.GetValue(tableAttr) as string;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
#region Context Pool
|
||||
|
||||
private static class ToonSerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<ToonSerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ToonSerializationContext Get(AcToonSerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
return new ToonSerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(ToonSerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
/// <summary>
|
||||
/// Pooled context for Toon serialization.
|
||||
/// Handles output building, indentation, and reference tracking.
|
||||
/// </summary>
|
||||
private sealed class ToonSerializationContext
|
||||
{
|
||||
private readonly StringBuilder _builder;
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, int>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private HashSet<Type>? _registeredTypes;
|
||||
private int _nextRefId;
|
||||
|
||||
public AcToonSerializerOptions Options { get; private set; }
|
||||
public int CurrentIndentLevel { get; set; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
public ToonSerializationContext(AcToonSerializerOptions options)
|
||||
{
|
||||
_builder = new StringBuilder(4096);
|
||||
Options = options;
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(AcToonSerializerOptions options)
|
||||
{
|
||||
Options = options;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
CurrentIndentLevel = 0;
|
||||
_nextRefId = 1;
|
||||
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
}
|
||||
|
||||
_registeredTypes ??= new HashSet<Type>(16);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_builder.Clear();
|
||||
_scanOccurrences?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
_registeredTypes?.Clear();
|
||||
CurrentIndentLevel = 0;
|
||||
_nextRefId = 1;
|
||||
}
|
||||
|
||||
#region Reference Handling
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
if (_scanOccurrences == null) return true;
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced!.Add(obj);
|
||||
return false;
|
||||
}
|
||||
count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteRef(object obj, out int refId)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
|
||||
{
|
||||
refId = _nextRefId++;
|
||||
return true;
|
||||
}
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId) => _writtenRefs![obj] = refId;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId)
|
||||
{
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
||||
return true;
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Registration
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool RegisterType(Type type)
|
||||
{
|
||||
if (_registeredTypes == null) return false;
|
||||
return _registeredTypes.Add(type);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsTypeRegistered(Type type)
|
||||
{
|
||||
return _registeredTypes?.Contains(type) ?? false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output Methods
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(string text)
|
||||
{
|
||||
_builder.Append(text);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(char c)
|
||||
{
|
||||
_builder.Append(c);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLine()
|
||||
{
|
||||
_builder.AppendLine();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
_builder.AppendLine(text);
|
||||
}
|
||||
|
||||
public void WriteIndent()
|
||||
{
|
||||
if (!Options.UseIndentation) return;
|
||||
|
||||
for (var i = 0; i < CurrentIndentLevel; i++)
|
||||
{
|
||||
_builder.Append(Options.IndentString);
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteIndentedLine(string text)
|
||||
{
|
||||
WriteIndent();
|
||||
WriteLine(text);
|
||||
}
|
||||
|
||||
public void WriteProperty(string name, string value, string? inlineComment = null)
|
||||
{
|
||||
WriteIndent();
|
||||
_builder.Append(name);
|
||||
|
||||
// Token optimization: no spaces around '=' when indentation is disabled
|
||||
if (Options.UseIndentation)
|
||||
{
|
||||
_builder.Append(" = ");
|
||||
}
|
||||
else
|
||||
{
|
||||
_builder.Append('=');
|
||||
}
|
||||
|
||||
_builder.Append(value);
|
||||
|
||||
if (inlineComment != null && Options.UseInlineComments)
|
||||
{
|
||||
_builder.Append(" # ");
|
||||
_builder.Append(inlineComment);
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public void WriteComment(string comment)
|
||||
{
|
||||
WriteIndent();
|
||||
_builder.Append("# ");
|
||||
_builder.AppendLine(comment);
|
||||
}
|
||||
|
||||
public string GetResult()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
public static partial class AcToonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached metadata for a type including properties, type name, and descriptions.
|
||||
/// </summary>
|
||||
private sealed class ToonTypeMetadata
|
||||
{
|
||||
public string TypeName { get; }
|
||||
public string ShortTypeName { get; }
|
||||
public ToonPropertyAccessor[] Properties { get; }
|
||||
public bool IsCollection { get; }
|
||||
public bool IsDictionary { get; }
|
||||
public Type? ElementType { get; }
|
||||
public ToonDescriptionAttribute? CustomDescription { get; }
|
||||
|
||||
public ToonTypeMetadata(Type type)
|
||||
{
|
||||
TypeName = type.FullName ?? type.Name;
|
||||
ShortTypeName = type.Name;
|
||||
|
||||
// Get custom attribute if present
|
||||
CustomDescription = type.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Check if collection or dictionary
|
||||
IsDictionary = IsDictionaryType(type, out _, out _);
|
||||
if (!IsDictionary)
|
||||
{
|
||||
ElementType = GetCollectionElementType(type);
|
||||
IsCollection = ElementType != null;
|
||||
}
|
||||
|
||||
// Build property accessors
|
||||
if (!IsCollection && !IsDictionary && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.CanRead &&
|
||||
p.GetIndexParameters().Length == 0 &&
|
||||
!HasJsonIgnoreAttribute(p))
|
||||
.Select(p => new ToonPropertyAccessor(p))
|
||||
.ToArray();
|
||||
|
||||
Properties = props;
|
||||
}
|
||||
else
|
||||
{
|
||||
Properties = Array.Empty<ToonPropertyAccessor>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property accessor with compiled getter for performance.
|
||||
/// </summary>
|
||||
private sealed class ToonPropertyAccessor
|
||||
{
|
||||
public string Name { get; }
|
||||
public Type PropertyType { get; }
|
||||
public PropertyInfo PropertyInfo { get; }
|
||||
public TypeCode PropertyTypeCode { get; }
|
||||
public string TypeDisplayName { get; }
|
||||
public bool IsNullable { get; }
|
||||
|
||||
// Custom attribute metadata
|
||||
public ToonDescriptionAttribute? CustomDescription { get; }
|
||||
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
public ToonPropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
Name = prop.Name;
|
||||
PropertyType = prop.PropertyType;
|
||||
PropertyInfo = prop;
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(PropertyType);
|
||||
IsNullable = underlyingType != null;
|
||||
|
||||
var actualType = underlyingType ?? PropertyType;
|
||||
PropertyTypeCode = Type.GetTypeCode(actualType);
|
||||
|
||||
// Build type display name for meta section
|
||||
TypeDisplayName = BuildTypeDisplayName(PropertyType);
|
||||
|
||||
// Get custom attribute if present
|
||||
CustomDescription = prop.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Compile getter for fast access
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _getter(obj);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if value is default/null without boxing value types.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsDefaultValue(object? value)
|
||||
{
|
||||
if (value == null) return true;
|
||||
|
||||
switch (PropertyTypeCode)
|
||||
{
|
||||
case TypeCode.Int32: return (int)value == 0;
|
||||
case TypeCode.Int64: return (long)value == 0L;
|
||||
case TypeCode.Double: return (double)value == 0.0;
|
||||
case TypeCode.Decimal: return (decimal)value == 0m;
|
||||
case TypeCode.Single: return (float)value == 0f;
|
||||
case TypeCode.Byte: return (byte)value == 0;
|
||||
case TypeCode.Int16: return (short)value == 0;
|
||||
case TypeCode.UInt16: return (ushort)value == 0;
|
||||
case TypeCode.UInt32: return (uint)value == 0;
|
||||
case TypeCode.UInt64: return (ulong)value == 0;
|
||||
case TypeCode.SByte: return (sbyte)value == 0;
|
||||
case TypeCode.Boolean: return (bool)value == false;
|
||||
case TypeCode.String: return string.IsNullOrEmpty((string)value);
|
||||
}
|
||||
|
||||
if (PropertyType.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (ReferenceEquals(PropertyType, GuidType)) return (Guid)value == Guid.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build human-readable type name for meta section.
|
||||
/// </summary>
|
||||
private static string BuildTypeDisplayName(Type type)
|
||||
{
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
var isNullable = underlying != null;
|
||||
var actualType = underlying ?? type;
|
||||
|
||||
var baseName = Type.GetTypeCode(actualType) switch
|
||||
{
|
||||
TypeCode.Int32 => "int32",
|
||||
TypeCode.Int64 => "int64",
|
||||
TypeCode.Double => "float64",
|
||||
TypeCode.Decimal => "decimal",
|
||||
TypeCode.Single => "float32",
|
||||
TypeCode.Boolean => "bool",
|
||||
TypeCode.String => "string",
|
||||
TypeCode.DateTime => "datetime",
|
||||
TypeCode.Byte => "byte",
|
||||
TypeCode.Int16 => "int16",
|
||||
TypeCode.UInt16 => "uint16",
|
||||
TypeCode.UInt32 => "uint32",
|
||||
TypeCode.UInt64 => "uint64",
|
||||
TypeCode.SByte => "sbyte",
|
||||
TypeCode.Char => "char",
|
||||
_ => GetComplexTypeName(actualType)
|
||||
};
|
||||
|
||||
return isNullable ? baseName + "?" : baseName;
|
||||
}
|
||||
|
||||
private static string GetComplexTypeName(Type type)
|
||||
{
|
||||
if (ReferenceEquals(type, GuidType)) return "guid";
|
||||
if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
|
||||
if (ReferenceEquals(type, TimeSpanType)) return "timespan";
|
||||
if (type.IsEnum) return "enum";
|
||||
|
||||
// Check for collections
|
||||
var elementType = GetCollectionElementType(type);
|
||||
if (elementType != null)
|
||||
{
|
||||
var elementTypeName = BuildTypeDisplayName(elementType);
|
||||
return $"{elementTypeName}[]";
|
||||
}
|
||||
|
||||
// Check for dictionaries
|
||||
if (IsDictionaryType(type, out var keyType, out var valueType))
|
||||
{
|
||||
var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object";
|
||||
var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object";
|
||||
return $"dict<{keyTypeName}, {valueTypeName}>";
|
||||
}
|
||||
|
||||
return type.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
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
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, ToonTypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
break;
|
||||
|
||||
case ToonSerializationMode.DataOnly:
|
||||
WriteDataSectionOnly(value, type, context);
|
||||
break;
|
||||
|
||||
case ToonSerializationMode.Full:
|
||||
default:
|
||||
WriteMetaSection(type, context);
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// Serialize only type metadata (schema) for a given type.
|
||||
/// </summary>
|
||||
public static string SerializeTypeMetadata(Type type)
|
||||
{
|
||||
var context = ToonSerializationContextPool.Get(AcToonSerializerOptions.MetaOnly);
|
||||
try
|
||||
{
|
||||
WriteMetaSectionOnly(type, context);
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
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)
|
||||
{
|
||||
return SerializeMetadata((IEnumerable<Type>)types);
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ToonTypeMetadata GetTypeMetadata(Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new ToonTypeMetadata(t));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
/// <summary>
|
||||
/// Controls what sections are included in Toon serialization output.
|
||||
/// </summary>
|
||||
public enum ToonSerializationMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Include both @meta/@types and @data sections (default).
|
||||
/// Best for first-time serialization or when LLM needs full context.
|
||||
/// </summary>
|
||||
Full = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only include @meta and @types sections, no @data.
|
||||
/// Use to send schema/documentation once at the start of conversation.
|
||||
/// </summary>
|
||||
MetaOnly = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only include @data section, no @meta/@types.
|
||||
/// Use when schema was already sent via MetaOnly - saves tokens.
|
||||
/// </summary>
|
||||
DataOnly = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for AcToonSerializer - Token-Oriented Object Notation.
|
||||
/// Optimized for LLM readability and token efficiency.
|
||||
/// </summary>
|
||||
public sealed class AcToonSerializerOptions : AcSerializerOptions
|
||||
{
|
||||
public override AcSerializerType SerializerType { get; init; } = AcSerializerType.Toon;
|
||||
|
||||
// === META CONTROL ===
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include type metadata (schema, descriptions, constraints).
|
||||
/// When true, @types section is included with full documentation.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseMeta { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Serialization mode - controls what gets serialized.
|
||||
/// Full = meta + data, MetaOnly = only schema, DataOnly = only values.
|
||||
/// Default: Full
|
||||
/// </summary>
|
||||
public ToonSerializationMode Mode { get; init; } = ToonSerializationMode.Full;
|
||||
|
||||
// === FORMATTING ===
|
||||
|
||||
/// <summary>
|
||||
/// Use indentation for readability.
|
||||
/// When false, output is more compact but harder to read.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseIndentation { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Indent string (spaces or tabs).
|
||||
/// Default: " " (2 spaces)
|
||||
/// </summary>
|
||||
public string IndentString { get; init; } = " ";
|
||||
|
||||
// === VERBOSITY ===
|
||||
|
||||
/// <summary>
|
||||
/// Include type hints inline with values (e.g., Age = 30 <int32>).
|
||||
/// Useful for debugging but increases output size.
|
||||
/// Default: false (types only in @types section)
|
||||
/// </summary>
|
||||
public bool UseInlineTypeHints { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Include property descriptions inline as comments.
|
||||
/// Useful for self-documenting output but increases size.
|
||||
/// Default: false (descriptions only in @types section)
|
||||
/// </summary>
|
||||
public bool UseInlineComments { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Show array/collection count in output (e.g., Tags: <string[]> (count: 3)).
|
||||
/// Helps LLM understand collection size at a glance.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool ShowCollectionCount { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Use multi-line string format for strings longer than threshold.
|
||||
/// Strings use triple-quote syntax: """..."""
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseMultiLineStrings { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length to trigger multi-line format.
|
||||
/// Shorter strings remain inline with escaping.
|
||||
/// Default: 80 characters
|
||||
/// </summary>
|
||||
public int MultiLineStringThreshold { get; init; } = 80;
|
||||
|
||||
/// <summary>
|
||||
/// Include enhanced property metadata (constraints, examples, purpose).
|
||||
/// Provides richer context for LLM understanding.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool UseEnhancedMetadata { get; init; } = true;
|
||||
|
||||
// === DATA CONTROL ===
|
||||
|
||||
/// <summary>
|
||||
/// Omit properties with default/null values.
|
||||
/// Reduces output size significantly for sparse objects.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool OmitDefaultValues { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Write type names for root objects (e.g., Person { ... } vs just { ... }).
|
||||
/// Helps LLM understand object types in data section.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public bool WriteTypeNames { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum string length before truncation in meta examples.
|
||||
/// Default: 50 characters
|
||||
/// </summary>
|
||||
public int MaxExampleStringLength { get; init; } = 50;
|
||||
|
||||
// === PREDEFINED MODES ===
|
||||
|
||||
/// <summary>
|
||||
/// Full mode: Meta + Data (first-time serialization).
|
||||
/// Use when LLM needs complete context about data structure and values.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions Default = new()
|
||||
{
|
||||
Mode = ToonSerializationMode.Full,
|
||||
UseMeta = true,
|
||||
UseIndentation = true,
|
||||
OmitDefaultValues = true,
|
||||
WriteTypeNames = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Meta-only mode: Only serialize type definitions and descriptions.
|
||||
/// Use this to send schema information once at conversation start.
|
||||
/// Subsequent serializations can use DataOnly mode to save tokens.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions MetaOnly = new()
|
||||
{
|
||||
Mode = ToonSerializationMode.MetaOnly,
|
||||
UseMeta = true,
|
||||
UseIndentation = true,
|
||||
UseInlineComments = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Data-only mode: Only serialize actual data values.
|
||||
/// Use this when schema was already sent via MetaOnly.
|
||||
/// Saves ~30-50% tokens in repeated serializations.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions DataOnly = new()
|
||||
{
|
||||
Mode = ToonSerializationMode.DataOnly,
|
||||
UseMeta = false,
|
||||
UseIndentation = true,
|
||||
OmitDefaultValues = true,
|
||||
WriteTypeNames = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Compact mode: Minimal output, no meta, no indentation.
|
||||
/// Maximum token efficiency but less readable.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions Compact = new()
|
||||
{
|
||||
Mode = ToonSerializationMode.DataOnly,
|
||||
UseMeta = false,
|
||||
UseIndentation = false,
|
||||
OmitDefaultValues = true,
|
||||
WriteTypeNames = false,
|
||||
UseReferenceHandling = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Verbose mode: Everything included (for debugging/documentation).
|
||||
/// Use when you need maximum information and clarity.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions Verbose = new()
|
||||
{
|
||||
Mode = ToonSerializationMode.Full,
|
||||
UseMeta = true,
|
||||
UseIndentation = true,
|
||||
UseInlineTypeHints = true,
|
||||
UseInlineComments = true,
|
||||
OmitDefaultValues = false,
|
||||
WriteTypeNames = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
/// </summary>
|
||||
public static AcToonSerializerOptions WithMaxDepth(byte maxDepth) => new() { MaxDepth = maxDepth };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without reference handling (faster, no circular reference support).
|
||||
/// </summary>
|
||||
public static AcToonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the type of relationship between entities.
|
||||
/// </summary>
|
||||
public enum ToonRelationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Many-to-one relationship (e.g., Person.Company -> Company).
|
||||
/// </summary>
|
||||
ManyToOne,
|
||||
|
||||
/// <summary>
|
||||
/// One-to-many relationship (e.g., Company.Employees -> Person[]).
|
||||
/// </summary>
|
||||
OneToMany,
|
||||
|
||||
/// <summary>
|
||||
/// One-to-one relationship.
|
||||
/// </summary>
|
||||
OneToOne,
|
||||
|
||||
/// <summary>
|
||||
/// Many-to-many relationship.
|
||||
/// </summary>
|
||||
ManyToMany
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides custom description metadata for Toon serialization with flexible fallback and placeholder support.
|
||||
/// This attribute can be applied to classes and properties to provide rich contextual information
|
||||
/// that will be included in the @types section of serialized output.
|
||||
///
|
||||
/// <para><b>KEY FEATURES:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>Partial Support:</b> You can specify only some properties (e.g., just Constraints), others will use fallbacks</item>
|
||||
/// <item><b>Placeholder System:</b> Use [#AttributeName] to reference Microsoft DataAnnotations or smart inference</item>
|
||||
/// <item><b>Fallback Chain:</b> ToonDescription → Microsoft Attributes → Smart Inference (automatic)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>FALLBACK PRIORITIES:</b></para>
|
||||
/// <list type="number">
|
||||
/// <item><b>Description:</b> ToonDescription.Description → [Description] → Smart Inference</item>
|
||||
/// <item><b>Purpose:</b> ToonDescription.Purpose → Smart Inference</item>
|
||||
/// <item><b>Constraints:</b> ToonDescription.Constraints → [Range]/[Required]/etc → Type Constraints → Smart Inference</item>
|
||||
/// <item><b>Examples:</b> ToonDescription.Examples (no automatic fallback)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para><b>SUPPORTED PLACEHOLDERS:</b></para>
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Placeholder</term>
|
||||
/// <description>Resolves To (Where: C=Class, D=Description, P=Purpose, C=Constraints, E=Examples)</description>
|
||||
/// </listheader>
|
||||
/// <item><term>[#Description]</term><description>Microsoft [Description] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#DisplayName]</term><description>Microsoft [DisplayName] attribute (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartDescription]</term><description>Auto-inferred description (Class.D, Property.D)</description></item>
|
||||
/// <item><term>[#SmartPurpose]</term><description>Auto-inferred purpose (Property.P only, empty for classes)</description></item>
|
||||
/// <item><term>[#Range]</term><description>Microsoft [Range] attribute → "range: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#Required]</term><description>Microsoft [Required] attribute → "required" (Property.C)</description></item>
|
||||
/// <item><term>[#MaxLength]</term><description>Microsoft [MaxLength] attribute → "max-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#MinLength]</term><description>Microsoft [MinLength] attribute → "min-length: N" (Property.C)</description></item>
|
||||
/// <item><term>[#StringLength]</term><description>Microsoft [StringLength] attribute → "length: min-max" (Property.C)</description></item>
|
||||
/// <item><term>[#EmailAddress]</term><description>Microsoft [EmailAddress] attribute → "email-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Phone]</term><description>Microsoft [Phone] attribute → "phone-format" (Property.C)</description></item>
|
||||
/// <item><term>[#Url]</term><description>Microsoft [Url] attribute → "url-format" (Property.C)</description></item>
|
||||
/// <item><term>[#CreditCard]</term><description>Microsoft [CreditCard] attribute → "credit-card-format" (Property.C)</description></item>
|
||||
/// <item><term>[#RegularExpression]</term><description>Microsoft [RegularExpression] → "pattern: ..." (Property.C)</description></item>
|
||||
/// <item><term>[#SmartTypeConstraints]</term><description>Type-derived constraints (nullable, numeric, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartInferenceConstraints]</term><description>Auto-inferred constraints (email-format, range, etc.) (Property.C)</description></item>
|
||||
/// <item><term>[#SmartGeneratedExample]</term><description>Auto-generated example value (Property.E)</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>USAGE MODES:</b></para>
|
||||
///
|
||||
/// <para><b>1. FULL CUSTOM (all properties specified):</b></para>
|
||||
/// <code>
|
||||
/// [ToonDescription("User email address",
|
||||
/// Purpose = "Authentication and notifications",
|
||||
/// Constraints = "required, email-format, unique",
|
||||
/// Examples = "user@example.com, admin@company.com")]
|
||||
/// public string Email { get; set; }
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>2. PARTIAL (only some properties, rest auto-filled):</b></para>
|
||||
/// <code>
|
||||
/// [Description("Contact email")] // Microsoft attribute
|
||||
/// [Required]
|
||||
/// [EmailAddress]
|
||||
/// [ToonDescription(Constraints = "[#Required], [#EmailAddress], unique")] // Only constraints specified
|
||||
/// public string Email { get; set; }
|
||||
/// // Result:
|
||||
/// // Description: "Contact email" (from Microsoft [Description])
|
||||
/// // Constraints: "required, email-format, unique" (merged with placeholders)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>3. PLACEHOLDER APPEND MODE (merge with Microsoft attributes):</b></para>
|
||||
/// <code>
|
||||
/// [Range(0, 150)]
|
||||
/// [Required]
|
||||
/// [ToonDescription(Constraints = "[#Required], [#Range], verified-by-admin")]
|
||||
/// public int Age { get; set; }
|
||||
/// // Result: "required, range: 0-150, verified-by-admin" (merged)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>4. REPLACE MODE (no placeholders = full override):</b></para>
|
||||
/// <code>
|
||||
/// [Range(0, 150)] // This is IGNORED
|
||||
/// [ToonDescription(Constraints = "custom-validation-only")]
|
||||
/// public int Score { get; set; }
|
||||
/// // Result: "custom-validation-only" (Microsoft attributes ignored)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>5. FULL AUTOMATIC (no ToonDescription at all):</b></para>
|
||||
/// <code>
|
||||
/// [Required]
|
||||
/// [EmailAddress]
|
||||
/// [MaxLength(100)]
|
||||
/// public string Email { get; set; }
|
||||
/// // Result:
|
||||
/// // Description: "Email address" (smart inference)
|
||||
/// // Constraints: "required, email-format, max-length: 100" (from Microsoft attributes)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>6. COMBINING PLACEHOLDERS IN DESCRIPTION:</b></para>
|
||||
/// <code>
|
||||
/// [DisplayName("User Email")]
|
||||
/// [Description("Contact email address")]
|
||||
/// [ToonDescription(Description = "[#DisplayName]: [#Description] (primary contact)")]
|
||||
/// public string Email { get; set; }
|
||||
/// // Result: "User Email: Contact email address (primary contact)"
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>7. CLASS-LEVEL WITH PLACEHOLDERS AND FALLBACK:</b></para>
|
||||
/// <code>
|
||||
/// // With Microsoft attribute
|
||||
/// [Description("Base user entity")]
|
||||
/// [ToonDescription("[#Description] with extended functionality",
|
||||
/// Purpose = "Manages user authentication and authorization")]
|
||||
/// public class User { }
|
||||
/// // Result:
|
||||
/// // Description: "Base user entity with extended functionality"
|
||||
/// // Purpose: "Manages user authentication and authorization"
|
||||
///
|
||||
/// // With inheritance (Inherited = true)
|
||||
/// [ToonDescription("Admin user account",
|
||||
/// Purpose = "Administrative users with elevated privileges")]
|
||||
/// public class AdminUser : User { }
|
||||
/// // Result: "Admin user account" (own description)
|
||||
///
|
||||
/// public class SuperUser : AdminUser { }
|
||||
/// // Result: "Admin user account" (inherited from AdminUser)
|
||||
///
|
||||
/// // Without any ToonDescription
|
||||
/// public class GuestUser : User { }
|
||||
/// // Result: "Object of type GuestUser" (smart inference)
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>BEST PRACTICES:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Use placeholders ([#...]) when you want to MERGE with existing Microsoft attributes</item>
|
||||
/// <item>Omit placeholders when you want to REPLACE (full custom control)</item>
|
||||
/// <item>Leave properties empty to use automatic fallbacks</item>
|
||||
/// <item>Combine placeholders with custom text for rich, DRY documentation</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
///
|
||||
/// <example>
|
||||
/// <para><b>COMPLETE EXAMPLE:</b></para>
|
||||
/// <code>
|
||||
/// [ToonDescription("Represents a user account in the system",
|
||||
/// Purpose = "User authentication and profile management")]
|
||||
/// public class Person
|
||||
/// {
|
||||
/// // Full custom
|
||||
/// [ToonDescription("Unique identifier",
|
||||
/// Purpose = "Primary key / database identity",
|
||||
/// Constraints = "required, auto-increment, positive")]
|
||||
/// public int Id { get; set; }
|
||||
///
|
||||
/// // Merge with Microsoft attributes
|
||||
/// [Range(18, 120)]
|
||||
/// [Required]
|
||||
/// [ToonDescription(Constraints = "[#Required], [#Range], verified")]
|
||||
/// public int Age { get; set; }
|
||||
/// // Result: "required, range: 18-120, verified"
|
||||
///
|
||||
/// // Partial - only constraints, rest auto-filled
|
||||
/// [Description("Contact email")]
|
||||
/// [EmailAddress]
|
||||
/// [ToonDescription(Constraints = "[#EmailAddress], unique")]
|
||||
/// public string Email { get; set; }
|
||||
/// // Description: "Contact email" (from Microsoft)
|
||||
/// // Constraints: "email-format, unique" (merged)
|
||||
///
|
||||
/// // Fully automatic - no ToonDescription
|
||||
/// [Required]
|
||||
/// [MaxLength(100)]
|
||||
/// public string Name { get; set; }
|
||||
/// // Description: "Name of the Person" (smart inference)
|
||||
/// // Constraints: "required, max-length: 100" (from Microsoft attributes)
|
||||
///
|
||||
/// // Placeholder in description
|
||||
/// [DisplayName("Account Balance")]
|
||||
/// [ToonDescription(Description = "[#DisplayName] in USD",
|
||||
/// Constraints = "range: 0-999999, decimal(18,2)",
|
||||
/// Examples = "0.00, 1234.56, 999999.99")]
|
||||
/// public decimal Balance { get; set; }
|
||||
/// // Description: "Account Balance in USD"
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ToonDescriptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the human-readable description of the property or type.
|
||||
/// This appears in the @types section to help LLMs understand the data structure.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the purpose of this property (what it's used for).
|
||||
/// Examples: "Primary key", "User authentication", "Audit trail".
|
||||
/// </summary>
|
||||
public string? Purpose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the constraints or validation rules for this property.
|
||||
/// Examples: "required, email-format", "range: 0-150", "max-length: 100".
|
||||
/// </summary>
|
||||
public string? Constraints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets example values for this property.
|
||||
/// Helps LLMs understand the expected format and content.
|
||||
/// </summary>
|
||||
public string? Examples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this property is a primary key.
|
||||
/// If not explicitly set, convention-based detection will be used (e.g., property named "Id").
|
||||
/// </summary>
|
||||
public bool? IsPrimaryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the foreign key navigation property name.
|
||||
/// If not explicitly set, convention-based detection will be used (e.g., "CompanyId" -> "Company").
|
||||
/// Example: For a property "CompanyId", set ForeignKey = "Company" to indicate it references the Company navigation property.
|
||||
/// </summary>
|
||||
public string? ForeignKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the relationship type for navigation properties.
|
||||
/// If not explicitly set, convention-based detection will be used based on property type.
|
||||
/// </summary>
|
||||
public ToonRelationType? Navigation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the inverse navigation property name for bidirectional relationships.
|
||||
/// Example: For Company.Employees, set InverseProperty = "Company" to indicate the inverse property on Person.
|
||||
/// </summary>
|
||||
public string? InverseProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database table name for this entity (class-level only).
|
||||
/// If not explicitly set, will fallback to EF Core [Table] or Linq2Db [Table] attributes, then to class name.
|
||||
/// Example: TableName = "tbl_Persons" for custom table naming.
|
||||
/// </summary>
|
||||
public string? TableName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ToonDescriptionAttribute with the specified description.
|
||||
/// </summary>
|
||||
/// <param name="description">Human-readable description of the property or type.</param>
|
||||
public ToonDescriptionAttribute(string description)
|
||||
{
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this attribute for debugging.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var parts = new List<string> { $"Description: {Description}" };
|
||||
if (!string.IsNullOrEmpty(Purpose))
|
||||
parts.Add($"Purpose: {Purpose}");
|
||||
if (!string.IsNullOrEmpty(Constraints))
|
||||
parts.Add($"Constraints: {Constraints}");
|
||||
if (!string.IsNullOrEmpty(Examples))
|
||||
parts.Add($"Examples: {Examples}");
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using AyCode.Core.Serializers.Jsons;
|
|||
using AyCode.Interfaces.Entities;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AyCode.Services.SignalRs
|
||||
{
|
||||
|
|
@ -40,6 +41,18 @@ namespace AyCode.Services.SignalRs
|
|||
options.CloseTimeout = TimeSpan.FromSeconds(10);
|
||||
options.SkipNegotiation = true;
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
// alap minimális MS log level
|
||||
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Warning);
|
||||
|
||||
// regisztráljuk az AcLoggerProvider-t úgy, hogy visszaadja a meglévő Logger példányt
|
||||
logging.AddAcLogger(_ => Logger);
|
||||
|
||||
// ha inkább csak AcLogger legyen:
|
||||
// logging.ClearProviders();
|
||||
// logging.AddProvider(new AcLoggerProvider<AcLoggerBase>(category => Logger));
|
||||
})
|
||||
.WithAutomaticReconnect()
|
||||
.WithStatefulReconnect()
|
||||
.WithKeepAliveInterval(TimeSpan.FromSeconds(60))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,1185 @@
|
|||
# AcToonSerializer - Complete Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
**Token-Oriented Object Notation (Toon)** is a revolutionary serialization format specifically designed for Large Language Models (LLMs) like Claude, GPT-4, and others. Unlike JSON or XML, Toon prioritizes **maximum clarity and understanding** for AI systems while maintaining human readability.
|
||||
|
||||
### Key Design Goals
|
||||
1. **LLM-First**: Every design decision optimized for AI comprehension
|
||||
2. **Zero Ambiguity**: Explicit structure markers eliminate parsing uncertainty
|
||||
3. **Context-Aware**: Rich metadata provides semantic understanding
|
||||
4. **Token Efficient**: Smart separation of schema and data
|
||||
5. **Developer Friendly**: Works with or without custom attributes
|
||||
|
||||
---
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Three-Layer System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ @meta Section │ ← Version, format, type registry
|
||||
├─────────────────────────────────────┤
|
||||
│ @types Section │ ← Schema, descriptions, constraints
|
||||
├─────────────────────────────────────┤
|
||||
│ @data Section │ ← Actual values
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Why This Matters for LLMs
|
||||
|
||||
**Traditional JSON:**
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"email": "john@example.com",
|
||||
"tags": ["developer", "senior"]
|
||||
}
|
||||
```
|
||||
❌ LLM must infer: What is "id"? Is email validated? How many tags?
|
||||
|
||||
**Toon Format:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "User account entity"
|
||||
id: int32
|
||||
description: "Unique identifier"
|
||||
purpose: "Primary key"
|
||||
constraints: "required, auto-increment"
|
||||
email: string
|
||||
description: "Contact email"
|
||||
constraints: "required, email-format, unique"
|
||||
tags: string[]
|
||||
description: "User role tags"
|
||||
}
|
||||
|
||||
@data {
|
||||
Person {
|
||||
id = 42
|
||||
email = "john@example.com"
|
||||
tags = <string[]> (count: 2) [
|
||||
"developer"
|
||||
"senior"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
✅ LLM instantly knows: id is primary key, email is validated, exactly 2 tags
|
||||
|
||||
---
|
||||
|
||||
## Feature Showcase
|
||||
|
||||
### 1. Explicit Structure Boundaries
|
||||
|
||||
**Problem with indentation-only formats (YAML):**
|
||||
```yaml
|
||||
person:
|
||||
name: John
|
||||
address:
|
||||
street: Main St
|
||||
city: Springfield
|
||||
```
|
||||
❓ Where does `address` end? LLM must track indentation levels.
|
||||
|
||||
**Toon Solution:**
|
||||
```toon
|
||||
Person {
|
||||
Name = "John"
|
||||
Address {
|
||||
Street = "Main St"
|
||||
City = "Springfield"
|
||||
}
|
||||
}
|
||||
```
|
||||
✅ Clear `{}` boundaries - zero ambiguity
|
||||
|
||||
---
|
||||
|
||||
### 2. Meta/Data Separation (Token Efficiency)
|
||||
|
||||
**Multi-turn Conversation Pattern:**
|
||||
|
||||
**Turn 1: Send Schema Once**
|
||||
```csharp
|
||||
var meta = AcToonSerializer.Serialize(person, AcToonSerializerOptions.MetaOnly);
|
||||
// Output: Only @meta and @types sections
|
||||
```
|
||||
|
||||
Output (~500 tokens):
|
||||
```toon
|
||||
@meta {
|
||||
version = "1.0"
|
||||
types = ["Person", "Address", "Company"]
|
||||
}
|
||||
|
||||
@types {
|
||||
Person: "User account entity"
|
||||
Id: int32
|
||||
description: "Unique identifier"
|
||||
purpose: "Primary key"
|
||||
constraints: "required"
|
||||
Name: string
|
||||
description: "Full name"
|
||||
constraints: "required, max-length: 100"
|
||||
Email: string
|
||||
description: "Contact email"
|
||||
constraints: "required, email-format"
|
||||
// ... all properties
|
||||
}
|
||||
```
|
||||
|
||||
**Turn 2-N: Send Only Data**
|
||||
```csharp
|
||||
var data = AcToonSerializer.Serialize(person, AcToonSerializerOptions.DataOnly);
|
||||
// Output: Only @data section
|
||||
```
|
||||
|
||||
Output (~200 tokens):
|
||||
```toon
|
||||
@data {
|
||||
Person {
|
||||
Id = 42
|
||||
Name = "John Doe"
|
||||
Email = "john@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result: 60% token savings in subsequent requests!**
|
||||
|
||||
---
|
||||
|
||||
### 3. Type Hints Everywhere
|
||||
|
||||
**Arrays with Count:**
|
||||
```toon
|
||||
Employees = <Person[]> (count: 150) [
|
||||
Person { Id = 1, Name = "Alice" }
|
||||
Person { Id = 2, Name = "Bob" }
|
||||
// ... 148 more
|
||||
]
|
||||
```
|
||||
|
||||
✅ LLM instantly knows:
|
||||
- Collection type: Person array
|
||||
- Exact count: 150 employees
|
||||
- No need to iterate to count
|
||||
|
||||
**Dictionaries with Count:**
|
||||
```toon
|
||||
Metrics = <dict> (count: 5) {
|
||||
"Revenue" => 1500000.50
|
||||
"Growth" => 25.5
|
||||
"Expenses" => 800000.00
|
||||
"Profit" => 700000.50
|
||||
"Margin" => 46.67
|
||||
}
|
||||
```
|
||||
|
||||
✅ LLM sees structure immediately
|
||||
|
||||
**Inline Type Hints (Verbose Mode):**
|
||||
```toon
|
||||
@data {
|
||||
Person {
|
||||
Id = 42 <int32>
|
||||
Name = "John" <string>
|
||||
Age = 30 <int32>
|
||||
Balance = 1234.56 <decimal>
|
||||
IsActive = true <bool>
|
||||
CreatedAt = "2024-01-10T10:30:00Z" <datetime>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Custom Attributes for Explicit Documentation
|
||||
|
||||
**Define Rich Metadata:**
|
||||
```csharp
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
|
||||
[ToonDescription("Represents a user account in the system")]
|
||||
public class Person
|
||||
{
|
||||
[ToonDescription("Unique identifier for the person",
|
||||
Purpose = "Primary key / database identity",
|
||||
Constraints = "required, auto-increment, positive")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[ToonDescription("Email address for contact and authentication",
|
||||
Purpose = "User login and communication",
|
||||
Constraints = "required, email-format, unique",
|
||||
Examples = "user@example.com, admin@company.org")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[ToonDescription("Age in years",
|
||||
Constraints = "required, range: 0-150")]
|
||||
public int Age { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Generated Output:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "Represents a user account in the system"
|
||||
Id: int32
|
||||
description: "Unique identifier for the person"
|
||||
purpose: "Primary key / database identity"
|
||||
constraints: "required, auto-increment, positive"
|
||||
Email: string
|
||||
description: "Email address for contact and authentication"
|
||||
purpose: "User login and communication"
|
||||
constraints: "required, email-format, unique"
|
||||
examples: "user@example.com, admin@company.org"
|
||||
Age: int32
|
||||
description: "Age in years"
|
||||
constraints: "required, range: 0-150"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Smart Inference (No Attributes Required)
|
||||
|
||||
**Automatic Pattern Recognition:**
|
||||
|
||||
```csharp
|
||||
public class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string PhoneNumber { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool HasPremium { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int EmployeeCount { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Auto-Generated Descriptions:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "Object of type Person"
|
||||
Id: int32
|
||||
description: "Unique identifier for Person"
|
||||
purpose: "Primary key / unique identification"
|
||||
constraints: "required"
|
||||
Name: string
|
||||
description: "Name of the Person"
|
||||
constraints: "required"
|
||||
Email: string
|
||||
description: "Email address"
|
||||
constraints: "required, email-format"
|
||||
PhoneNumber: string
|
||||
description: "Phone number"
|
||||
constraints: "required"
|
||||
IsActive: bool
|
||||
description: "Boolean flag indicating Active"
|
||||
purpose: "Status flag"
|
||||
constraints: "required"
|
||||
HasPremium: bool
|
||||
description: "Boolean flag indicating possession of Premium"
|
||||
purpose: "Status flag"
|
||||
constraints: "required"
|
||||
CreatedAt: datetime
|
||||
description: "Date/time value for CreatedAt"
|
||||
purpose: "Timestamp when entity was created"
|
||||
constraints: "required"
|
||||
UpdatedAt: datetime?
|
||||
description: "Date/time value for UpdatedAt"
|
||||
purpose: "Timestamp of last update"
|
||||
constraints: "nullable"
|
||||
EmployeeCount: int32
|
||||
description: "Count of Employee"
|
||||
constraints: "required, non-negative"
|
||||
}
|
||||
```
|
||||
|
||||
**Detected Patterns:**
|
||||
- `Id` → Primary key
|
||||
- `Name` → Entity name
|
||||
- `Email`, `Phone`, `Address` → Contact info
|
||||
- `IsXxx`, `HasXxx` → Boolean flags
|
||||
- `CreatedAt`, `UpdatedAt`, `DeletedAt` → Audit timestamps
|
||||
- `XxxCount` → Counters (non-negative)
|
||||
|
||||
---
|
||||
|
||||
### 6. Multi-line String Support
|
||||
|
||||
**Problem with Escaped Strings:**
|
||||
```json
|
||||
{
|
||||
"bio": "Line 1\nLine 2\nLine 3\n\nSpecialties:\n- C#\n- .NET\n- Azure"
|
||||
}
|
||||
```
|
||||
❌ Hard to read, especially with code snippets
|
||||
|
||||
**Toon Solution:**
|
||||
```toon
|
||||
Bio = """
|
||||
Senior Software Engineer with 10+ years of experience.
|
||||
|
||||
Specialties:
|
||||
- C# and .NET development
|
||||
- Cloud architecture (Azure, AWS)
|
||||
- Microservices and distributed systems
|
||||
|
||||
Passionate about clean code and mentoring.
|
||||
"""
|
||||
```
|
||||
✅ Preserves formatting, easy to read
|
||||
|
||||
**Automatically Triggered:**
|
||||
- Strings > 80 characters (configurable)
|
||||
- Manual override available
|
||||
|
||||
---
|
||||
|
||||
### 7. Reference Handling for Circular Objects
|
||||
|
||||
**Circular Reference Example:**
|
||||
```csharp
|
||||
var company = new Company { Name = "ACME Corp" };
|
||||
var ceo = new Person { Name = "John Doe" };
|
||||
company.CEO = ceo;
|
||||
ceo.Company = company; // Circular!
|
||||
```
|
||||
|
||||
**Toon Output:**
|
||||
```toon
|
||||
@data {
|
||||
@1 Company {
|
||||
Name = "ACME Corp"
|
||||
CEO = @2 Person {
|
||||
Name = "John Doe"
|
||||
Company = @ref:1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
✅ `@1` marks first occurrence
|
||||
✅ `@ref:1` references it
|
||||
✅ No infinite loops or duplication
|
||||
|
||||
---
|
||||
|
||||
## Complete Usage Examples
|
||||
|
||||
### Example 1: E-commerce System
|
||||
|
||||
```csharp
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
|
||||
[ToonDescription("Online store product listing")]
|
||||
public class Product
|
||||
{
|
||||
[ToonDescription("Unique product identifier",
|
||||
Purpose = "Primary key",
|
||||
Constraints = "required, auto-increment")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[ToonDescription("Product display name",
|
||||
Constraints = "required, max-length: 200")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[ToonDescription("Detailed product description",
|
||||
Constraints = "nullable, max-length: 2000")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[ToonDescription("Price in USD",
|
||||
Constraints = "required, positive, precision: 2")]
|
||||
public decimal Price { get; set; }
|
||||
|
||||
[ToonDescription("Available inventory count",
|
||||
Constraints = "required, non-negative")]
|
||||
public int Stock { get; set; }
|
||||
|
||||
[ToonDescription("Product category tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
}
|
||||
|
||||
// Serialize
|
||||
var product = new Product
|
||||
{
|
||||
Id = 101,
|
||||
Name = "Premium Wireless Headphones",
|
||||
Description = "High-quality noise-canceling headphones.\n\nFeatures:\n- 40-hour battery\n- Active noise cancellation\n- Premium sound quality",
|
||||
Price = 299.99m,
|
||||
Stock = 47,
|
||||
Tags = new List<string> { "electronics", "audio", "premium" }
|
||||
};
|
||||
|
||||
var toon = AcToonSerializer.Serialize(product);
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```toon
|
||||
@meta {
|
||||
version = "1.0"
|
||||
format = "toon"
|
||||
types = ["Product"]
|
||||
}
|
||||
|
||||
@types {
|
||||
Product: "Online store product listing"
|
||||
Id: int32
|
||||
description: "Unique product identifier"
|
||||
purpose: "Primary key"
|
||||
constraints: "required, auto-increment"
|
||||
Name: string
|
||||
description: "Product display name"
|
||||
constraints: "required, max-length: 200"
|
||||
Description: string?
|
||||
description: "Detailed product description"
|
||||
constraints: "nullable, max-length: 2000"
|
||||
Price: decimal
|
||||
description: "Price in USD"
|
||||
constraints: "required, positive, precision: 2"
|
||||
Stock: int32
|
||||
description: "Available inventory count"
|
||||
constraints: "required, non-negative"
|
||||
Tags: string[]
|
||||
description: "Product category tags"
|
||||
constraints: "nullable"
|
||||
}
|
||||
|
||||
@data {
|
||||
Product {
|
||||
Id = 101
|
||||
Name = "Premium Wireless Headphones"
|
||||
Description = """
|
||||
High-quality noise-canceling headphones.
|
||||
|
||||
Features:
|
||||
- 40-hour battery
|
||||
- Active noise cancellation
|
||||
- Premium sound quality
|
||||
"""
|
||||
Price = 299.99
|
||||
Stock = 47
|
||||
Tags = <string[]> (count: 3) [
|
||||
"electronics"
|
||||
"audio"
|
||||
"premium"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Token-Efficient Workflow
|
||||
|
||||
```csharp
|
||||
// === TURN 1: Initial Request - Send Full Context ===
|
||||
var person = new Person { Id = 1, Name = "Alice", Email = "alice@example.com" };
|
||||
var fullToon = AcToonSerializer.Serialize(person, AcToonSerializerOptions.Default);
|
||||
// LLM learns schema (~600 tokens)
|
||||
|
||||
// === TURN 2-10: Updates - Send Only Data ===
|
||||
var updates = new[]
|
||||
{
|
||||
new Person { Id = 2, Name = "Bob", Email = "bob@example.com" },
|
||||
new Person { Id = 3, Name = "Charlie", Email = "charlie@example.com" },
|
||||
new Person { Id = 4, Name = "Diana", Email = "diana@example.com" }
|
||||
};
|
||||
|
||||
foreach (var update in updates)
|
||||
{
|
||||
var dataToon = AcToonSerializer.Serialize(update, AcToonSerializerOptions.DataOnly);
|
||||
// Each ~150 tokens instead of ~600
|
||||
// Total savings: 450 tokens × 3 = 1,350 tokens saved!
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Preset Modes
|
||||
|
||||
```csharp
|
||||
// 1. Default - Full context (first time)
|
||||
AcToonSerializerOptions.Default
|
||||
|
||||
// 2. MetaOnly - Schema only (send once)
|
||||
AcToonSerializerOptions.MetaOnly
|
||||
|
||||
// 3. DataOnly - Values only (subsequent requests)
|
||||
AcToonSerializerOptions.DataOnly
|
||||
|
||||
// 4. Compact - Minimal output (no indentation)
|
||||
AcToonSerializerOptions.Compact
|
||||
|
||||
// 5. Verbose - All hints inline (debugging)
|
||||
AcToonSerializerOptions.Verbose
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
```csharp
|
||||
var options = new AcToonSerializerOptions
|
||||
{
|
||||
Mode = ToonSerializationMode.Full,
|
||||
UseMeta = true,
|
||||
UseEnhancedMetadata = true,
|
||||
ShowCollectionCount = true,
|
||||
UseMultiLineStrings = true,
|
||||
MultiLineStringThreshold = 80,
|
||||
UseInlineTypeHints = false,
|
||||
OmitDefaultValues = true,
|
||||
UseReferenceHandling = true,
|
||||
MaxDepth = 10
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Token Efficiency
|
||||
|
||||
| Scenario | JSON | Toon Full | Toon DataOnly | Savings |
|
||||
|----------|------|-----------|---------------|---------|
|
||||
| First Request | 800 | 1000 | - | -25% |
|
||||
| Subsequent (×10) | 8000 | - | 4000 | **50%** |
|
||||
| **Total Conversation** | **8800** | - | **5000** | **43%** |
|
||||
|
||||
### Speed Benchmarks
|
||||
|
||||
```
|
||||
Serialization Speed (relative to JSON):
|
||||
- First time (Full): ~85% (builds metadata cache)
|
||||
- Subsequent (DataOnly): ~95% (cache hit)
|
||||
- With attributes: ~90% (reflection overhead)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Toon is Superior for LLMs
|
||||
|
||||
### 1. **Cognitive Load Reduction**
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{"users": [{"id": 1}, {"id": 2}]}
|
||||
```
|
||||
LLM thinks: *"What's in users? How many? What properties exist?"*
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
users = <Person[]> (count: 2) [
|
||||
Person { id = 1 }
|
||||
Person { id = 2 }
|
||||
]
|
||||
```
|
||||
LLM knows: *"Array of Person, exactly 2 items, each has id property"*
|
||||
|
||||
### 2. **Semantic Understanding**
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{"email": "test@example.com"}
|
||||
```
|
||||
LLM: *"Is this validated? Required? Format?"*
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
email: string
|
||||
description: "Contact email"
|
||||
constraints: "required, email-format, unique"
|
||||
```
|
||||
LLM: *"Must be valid email, required, unique in system"*
|
||||
|
||||
### 3. **Context Preservation**
|
||||
|
||||
**Multi-turn JSON:**
|
||||
```
|
||||
Turn 1: {"id": 1, "name": "Alice"}
|
||||
Turn 2: {"id": 2, "name": "Bob"}
|
||||
Turn 3: {"id": 3, "name": "Charlie"}
|
||||
```
|
||||
LLM: *"Same structure? Any changes? Must infer each time"*
|
||||
|
||||
**Multi-turn Toon:**
|
||||
```
|
||||
Turn 1: @types { Person: ... } @data { ... }
|
||||
Turn 2: @data { Person { id = 2 } }
|
||||
Turn 3: @data { Person { id = 3 } }
|
||||
```
|
||||
LLM: *"Schema known from Turn 1, only data changes"*
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use MetaOnly/DataOnly Pattern
|
||||
|
||||
```csharp
|
||||
// Start of conversation
|
||||
var schemaToon = AcToonSerializer.Serialize(typeof(MyClass), AcToonSerializerOptions.MetaOnly);
|
||||
await SendToLLM(schemaToon);
|
||||
|
||||
// Subsequent messages
|
||||
var dataToon = AcToonSerializer.Serialize(instance, AcToonSerializerOptions.DataOnly);
|
||||
await SendToLLM(dataToon);
|
||||
```
|
||||
|
||||
### 2. Add Custom Attributes for Domain Models
|
||||
|
||||
```csharp
|
||||
[ToonDescription("Core business entity")]
|
||||
public class Customer
|
||||
{
|
||||
[ToonDescription("Customer identifier", Purpose = "Primary key")]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Rely on Smart Inference for DTOs
|
||||
|
||||
```csharp
|
||||
// No attributes needed - smart inference handles it
|
||||
public class UserDto
|
||||
{
|
||||
public int Id { get; set; } // → "Unique identifier"
|
||||
public string Email { get; set; } // → "Email address"
|
||||
public bool IsActive { get; set; } // → "Boolean flag"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**AcToonSerializer** is the first serialization format designed specifically for LLM understanding:
|
||||
|
||||
✅ **Zero Ambiguity** - Explicit boundaries (`{}`, `[]`)
|
||||
✅ **Rich Context** - Descriptions, constraints, purpose
|
||||
✅ **Token Efficient** - 30-50% savings with Meta/Data split
|
||||
✅ **Type Clear** - Count hints, type annotations
|
||||
✅ **Flexible** - Works with or without custom attributes
|
||||
✅ **Smart** - Auto-infers common patterns
|
||||
✅ **Complete** - Handles circular refs, multi-line strings, all C# types
|
||||
|
||||
**Result: LLMs understand your data structures perfectly with minimal token cost!**
|
||||
|
||||
---
|
||||
|
||||
## Toon vs JSON vs XML - Comprehensive Comparison
|
||||
|
||||
### Overview Table
|
||||
|
||||
| Feature | Toon | JSON | XML |
|
||||
|---------|------|------|-----|
|
||||
| **LLM Readability** | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good | ⭐⭐ Fair |
|
||||
| **Human Readability** | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Good | ⭐⭐ Fair |
|
||||
| **Structure Clarity** | Explicit `{}` `[]` | Implicit (commas) | Verbose tags |
|
||||
| **Type Information** | Built-in + hints | None | Via schema only |
|
||||
| **Metadata Support** | Rich (desc, purpose, constraints) | None | Via schema only |
|
||||
| **Schema Separation** | Yes (@meta/@types/@data) | No | External XSD |
|
||||
| **Token Efficiency** | ⭐⭐⭐⭐⭐ (43% savings) | ⭐⭐⭐ Baseline | ⭐ Verbose |
|
||||
| **Multi-line Strings** | Native `"""` | Escaped `\n` | CDATA or escaped |
|
||||
| **Collection Count** | Yes `<type[]> (count: N)` | No | No |
|
||||
| **Reference Handling** | Built-in `@1, @ref:1` | Manual | Via id/idref |
|
||||
| **Smart Inference** | Yes (15+ patterns) | No | No |
|
||||
| **Custom Attributes** | Yes (ToonDescription) | No | No |
|
||||
| **Parsing Complexity** | Simple | Simple | Complex |
|
||||
| **Size (bytes)** | Medium | Small | Large |
|
||||
| **Ambiguity Level** | Zero | Low | Medium |
|
||||
|
||||
---
|
||||
|
||||
### Detailed Comparison
|
||||
|
||||
#### 1. Structure Clarity
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
Person {
|
||||
Name = "John"
|
||||
Address {
|
||||
City = "NYC"
|
||||
}
|
||||
}
|
||||
```
|
||||
✅ Clear scope boundaries
|
||||
✅ Explicit start/end
|
||||
✅ No punctuation confusion
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{
|
||||
"person": {
|
||||
"name": "John",
|
||||
"address": {
|
||||
"city": "NYC"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
⚠️ Commas required
|
||||
⚠️ Easy to miss closing braces
|
||||
⚠️ No type information
|
||||
|
||||
**XML:**
|
||||
```xml
|
||||
<Person>
|
||||
<Name>John</Name>
|
||||
<Address>
|
||||
<City>NYC</City>
|
||||
</Address>
|
||||
</Person>
|
||||
```
|
||||
❌ Verbose
|
||||
❌ Opening/closing tags redundant
|
||||
❌ More bytes for same data
|
||||
|
||||
---
|
||||
|
||||
#### 2. Type Information & Metadata
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "User account entity"
|
||||
Id: int32
|
||||
description: "Unique identifier"
|
||||
purpose: "Primary key"
|
||||
constraints: "required, auto-increment"
|
||||
Email: string
|
||||
description: "Contact email"
|
||||
constraints: "required, email-format, unique"
|
||||
examples: "user@example.com"
|
||||
}
|
||||
```
|
||||
✅ Types inline
|
||||
✅ Rich metadata
|
||||
✅ Descriptions, constraints, purpose, examples
|
||||
✅ LLM understands semantics immediately
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
❌ No type info
|
||||
❌ No metadata
|
||||
❌ LLM must infer everything
|
||||
❌ Requires separate documentation
|
||||
|
||||
**XML with XSD:**
|
||||
```xml
|
||||
<!-- Data -->
|
||||
<Person>
|
||||
<Id>42</Id>
|
||||
<Email>user@example.com</Email>
|
||||
</Person>
|
||||
|
||||
<!-- Separate schema file -->
|
||||
<xs:schema>
|
||||
<xs:element name="Id" type="xs:int"/>
|
||||
<xs:element name="Email" type="xs:string"/>
|
||||
</xs:schema>
|
||||
```
|
||||
⚠️ Schema in separate file
|
||||
⚠️ Complex schema language
|
||||
⚠️ No semantic descriptions
|
||||
|
||||
---
|
||||
|
||||
#### 3. Collection Handling
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
Tags = <string[]> (count: 3) [
|
||||
"developer"
|
||||
"senior"
|
||||
"remote"
|
||||
]
|
||||
```
|
||||
✅ Type visible: `string[]`
|
||||
✅ Count visible: `3`
|
||||
✅ Clear boundaries
|
||||
✅ LLM knows structure instantly
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{
|
||||
"tags": ["developer", "senior", "remote"]
|
||||
}
|
||||
```
|
||||
⚠️ No type info (could be mixed types)
|
||||
⚠️ No count (must iterate)
|
||||
⚠️ Square brackets only marker
|
||||
|
||||
**XML:**
|
||||
```xml
|
||||
<Tags>
|
||||
<Tag>developer</Tag>
|
||||
<Tag>senior</Tag>
|
||||
<Tag>remote</Tag>
|
||||
</Tags>
|
||||
```
|
||||
❌ Verbose (3x more bytes)
|
||||
❌ No count
|
||||
❌ No type info
|
||||
❌ Repetitive tags
|
||||
|
||||
---
|
||||
|
||||
#### 4. Multi-line Strings
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
Bio = """
|
||||
Senior Software Engineer
|
||||
|
||||
Specialties:
|
||||
- C# Development
|
||||
- Cloud Architecture
|
||||
"""
|
||||
```
|
||||
✅ Natural formatting
|
||||
✅ Readable
|
||||
✅ No escaping needed
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{
|
||||
"bio": "Senior Software Engineer\n\nSpecialties:\n- C# Development\n- Cloud Architecture"
|
||||
}
|
||||
```
|
||||
❌ Escaped newlines
|
||||
❌ Hard to read
|
||||
❌ Error-prone
|
||||
|
||||
**XML:**
|
||||
```xml
|
||||
<Bio><![CDATA[
|
||||
Senior Software Engineer
|
||||
|
||||
Specialties:
|
||||
- C# Development
|
||||
- Cloud Architecture
|
||||
]]></Bio>
|
||||
```
|
||||
⚠️ CDATA verbose
|
||||
⚠️ Extra syntax
|
||||
|
||||
---
|
||||
|
||||
#### 5. Token Efficiency (Multi-turn Conversations)
|
||||
|
||||
**Scenario: 10-turn conversation with same schema**
|
||||
|
||||
**Toon:**
|
||||
- Turn 1: 1000 tokens (Full mode with @meta/@types/@data)
|
||||
- Turn 2-10: 200 tokens each (DataOnly mode)
|
||||
- **Total: 1000 + (9 × 200) = 2,800 tokens**
|
||||
|
||||
**JSON:**
|
||||
- Turn 1-10: 600 tokens each (no schema separation)
|
||||
- **Total: 10 × 600 = 6,000 tokens**
|
||||
|
||||
**XML:**
|
||||
- Turn 1-10: 900 tokens each (verbose)
|
||||
- **Total: 10 × 900 = 9,000 tokens**
|
||||
|
||||
**Result:**
|
||||
- Toon saves **53% vs JSON**
|
||||
- Toon saves **69% vs XML**
|
||||
|
||||
---
|
||||
|
||||
#### 6. Reference Handling (Circular Objects)
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
@1 Company {
|
||||
Name = "ACME"
|
||||
CEO = @2 Person {
|
||||
Name = "John"
|
||||
Company = @ref:1
|
||||
}
|
||||
}
|
||||
```
|
||||
✅ Built-in
|
||||
✅ Clear syntax
|
||||
✅ Automatic detection
|
||||
|
||||
**JSON (manual):**
|
||||
```json
|
||||
{
|
||||
"$id": "1",
|
||||
"name": "ACME",
|
||||
"ceo": {
|
||||
"$id": "2",
|
||||
"name": "John",
|
||||
"company": { "$ref": "1" }
|
||||
}
|
||||
}
|
||||
```
|
||||
⚠️ Manual implementation
|
||||
⚠️ Not standard
|
||||
⚠️ Library-dependent
|
||||
|
||||
**XML:**
|
||||
```xml
|
||||
<Company id="c1">
|
||||
<Name>ACME</Name>
|
||||
<CEO id="p1">
|
||||
<Name>John</Name>
|
||||
<Company idref="c1"/>
|
||||
</CEO>
|
||||
</Company>
|
||||
```
|
||||
⚠️ Attribute-based
|
||||
⚠️ Requires schema
|
||||
⚠️ Complex validation
|
||||
|
||||
---
|
||||
|
||||
#### 7. Semantic Understanding for LLMs
|
||||
|
||||
**Example: Understanding an "email" field**
|
||||
|
||||
**Toon:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "User account"
|
||||
Email: string
|
||||
description: "Contact email address"
|
||||
purpose: "User authentication and communication"
|
||||
constraints: "required, email-format, unique"
|
||||
examples: "user@example.com"
|
||||
}
|
||||
@data {
|
||||
Person { Email = "john@example.com" }
|
||||
}
|
||||
```
|
||||
|
||||
**LLM understands:**
|
||||
1. ✅ It's an email address (description)
|
||||
2. ✅ Used for authentication (purpose)
|
||||
3. ✅ Must be valid email format (constraints)
|
||||
4. ✅ Required field (constraints)
|
||||
5. ✅ Must be unique (constraints)
|
||||
6. ✅ Format example provided
|
||||
|
||||
**JSON:**
|
||||
```json
|
||||
{
|
||||
"email": "john@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**LLM infers:**
|
||||
1. ⚠️ Probably email (from name)
|
||||
2. ❌ Purpose unknown
|
||||
3. ❌ Constraints unknown
|
||||
4. ❌ Validation rules unknown
|
||||
5. ❌ Uniqueness unknown
|
||||
|
||||
**XML:**
|
||||
```xml
|
||||
<Email>john@example.com</Email>
|
||||
```
|
||||
|
||||
**LLM infers:**
|
||||
1. ⚠️ Probably email (from tag name)
|
||||
2. ❌ All other context missing
|
||||
|
||||
---
|
||||
|
||||
#### 8. Smart Inference (No Manual Documentation)
|
||||
|
||||
**Toon (Automatic):**
|
||||
```csharp
|
||||
public class Person
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Auto-generated:**
|
||||
```toon
|
||||
@types {
|
||||
Person: "Object of type Person"
|
||||
Id: int32
|
||||
description: "Unique identifier for Person"
|
||||
purpose: "Primary key / unique identification"
|
||||
Email: string
|
||||
description: "Email address"
|
||||
constraints: "required, email-format"
|
||||
IsActive: bool
|
||||
description: "Boolean flag indicating Active"
|
||||
purpose: "Status flag"
|
||||
CreatedAt: datetime
|
||||
description: "Date/time value for CreatedAt"
|
||||
purpose: "Timestamp when entity was created"
|
||||
}
|
||||
```
|
||||
✅ 15+ patterns recognized
|
||||
✅ Zero manual work
|
||||
✅ Intelligent descriptions
|
||||
|
||||
**JSON/XML:**
|
||||
❌ No automatic metadata
|
||||
❌ Requires manual documentation
|
||||
❌ No pattern recognition
|
||||
|
||||
---
|
||||
|
||||
#### 9. Real-World Size Comparison
|
||||
|
||||
**Sample: Person object with 10 properties**
|
||||
|
||||
**Toon (Full mode, first time):**
|
||||
```
|
||||
@meta + @types: 600 bytes
|
||||
@data: 250 bytes
|
||||
Total: 850 bytes
|
||||
```
|
||||
|
||||
**Toon (DataOnly mode, subsequent):**
|
||||
```
|
||||
@data only: 250 bytes
|
||||
```
|
||||
|
||||
**JSON:**
|
||||
```
|
||||
Data + field names: 400 bytes (every time)
|
||||
```
|
||||
|
||||
**XML:**
|
||||
```
|
||||
Data + tags (opening/closing): 800 bytes (every time)
|
||||
```
|
||||
|
||||
**10 requests total:**
|
||||
- Toon: 850 + (9 × 250) = **3,100 bytes**
|
||||
- JSON: 10 × 400 = **4,000 bytes** (+29%)
|
||||
- XML: 10 × 800 = **8,000 bytes** (+158%)
|
||||
|
||||
---
|
||||
|
||||
#### 10. Parsing Complexity for LLMs
|
||||
|
||||
**Toon:**
|
||||
1. Read @meta → know version, types
|
||||
2. Read @types → understand schema completely
|
||||
3. Read @data → parse values with full context
|
||||
4. Clear boundaries (`{}`, `[]`) → no ambiguity
|
||||
|
||||
**Complexity: ⭐ Low**
|
||||
|
||||
**JSON:**
|
||||
1. Parse entire structure
|
||||
2. Infer types from values
|
||||
3. Guess semantic meaning from keys
|
||||
4. Track nested braces and commas
|
||||
5. No schema context
|
||||
|
||||
**Complexity: ⭐⭐⭐ Medium**
|
||||
|
||||
**XML:**
|
||||
1. Parse opening/closing tags
|
||||
2. Match tag pairs
|
||||
3. Handle attributes vs elements
|
||||
4. External schema lookup (if used)
|
||||
5. Namespace handling
|
||||
6. CDATA sections
|
||||
|
||||
**Complexity: ⭐⭐⭐⭐⭐ High**
|
||||
|
||||
---
|
||||
|
||||
### Summary: Toon Advantages
|
||||
|
||||
#### vs JSON:
|
||||
|
||||
✅ **Semantic richness**: Descriptions, purpose, constraints, examples
|
||||
✅ **Type clarity**: Explicit types, not inferred
|
||||
✅ **Token efficiency**: 43% savings in conversations (Meta/Data split)
|
||||
✅ **Structure clarity**: Explicit boundaries vs implicit commas
|
||||
✅ **Smart inference**: Automatic metadata generation
|
||||
✅ **Multi-line strings**: Native support vs escaped
|
||||
✅ **Collection hints**: Count and type visible
|
||||
✅ **Reference handling**: Built-in vs manual
|
||||
✅ **LLM understanding**: Rich context vs bare values
|
||||
|
||||
**When to use JSON:** Legacy systems, browser APIs, minimal bandwidth (single request)
|
||||
|
||||
#### vs XML:
|
||||
|
||||
✅ **Conciseness**: 50-70% smaller
|
||||
✅ **Readability**: Clean syntax vs verbose tags
|
||||
✅ **Type information**: Inline vs external schema
|
||||
✅ **Metadata**: Built-in vs external
|
||||
✅ **Parsing**: Simple vs complex
|
||||
✅ **Modern**: Designed for LLMs vs 1998 technology
|
||||
✅ **Token efficiency**: 69% savings
|
||||
✅ **No redundancy**: Single property names vs opening/closing tags
|
||||
✅ **Clean collections**: Arrays vs repetitive elements
|
||||
|
||||
**When to use XML:** Legacy enterprise systems, SOAP, strict schema validation requirements
|
||||
|
||||
---
|
||||
|
||||
### The Toon Advantage: Real-World Impact
|
||||
|
||||
**Use Case: Multi-turn LLM conversation (analyzing 100 customer records)**
|
||||
|
||||
| Format | Tokens Used | Cost (Claude 3.5) | Processing Time |
|
||||
|--------|-------------|-------------------|-----------------|
|
||||
| Toon | 15,000 | $0.30 | Fast (schema parsed once) |
|
||||
| JSON | 35,000 | $0.70 | Medium (infer schema each time) |
|
||||
| XML | 52,000 | $1.04 | Slow (parse verbose structure) |
|
||||
|
||||
**Toon Savings:**
|
||||
- **57% fewer tokens** vs JSON
|
||||
- **71% fewer tokens** vs XML
|
||||
- **57% cost reduction** vs JSON
|
||||
- **71% cost reduction** vs XML
|
||||
- **Better LLM accuracy** (full semantic context)
|
||||
|
||||
---
|
||||
|
||||
### Conclusion
|
||||
|
||||
**Toon is superior when:**
|
||||
1. Working with LLMs (Claude, GPT-4, etc.)
|
||||
2. Multi-turn conversations (schema reuse)
|
||||
3. Need semantic understanding (not just data)
|
||||
4. Want automatic documentation
|
||||
5. Prefer clarity over brevity
|
||||
6. Handle complex object graphs
|
||||
7. Need both human and AI readability
|
||||
|
||||
**JSON is better when:**
|
||||
1. Browser/web API compatibility required
|
||||
2. Single-request scenarios
|
||||
3. Absolute minimum size critical
|
||||
4. No LLM processing involved
|
||||
|
||||
**XML is better when:**
|
||||
1. Legacy enterprise systems
|
||||
2. Strict schema validation via XSD
|
||||
3. SOAP/WS-* protocols
|
||||
4. Industry standards require it
|
||||
|
||||
**For modern LLM-powered applications, Toon is the clear winner.** 🏆
|
||||
Loading…
Reference in New Issue