Add Toon serializer: LLM-optimized format & logging

Introduced AcToonSerializer, a new high-performance, LLM-friendly serialization format (Toon) with explicit meta/types/data sections, rich type metadata, and smart inference for property documentation. Added ToonDescriptionAttribute for custom schema annotations and relationship metadata. Implemented fallback/placeholder system merging custom, Microsoft, and inferred metadata. Enhanced logging: AcLoggerBase now implements ILogger, with provider/extensions for DI integration. Updated SignalR client to use AcLogger. Added ToonExtendedInfo.txt with full documentation. No breaking changes to existing serialization.
This commit is contained in:
Loretta 2026-01-12 07:16:41 +01:00
commit 223036f8e9
20 changed files with 4832 additions and 20 deletions

View File

@ -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>

View File

@ -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}"

View File

@ -17,6 +17,7 @@
<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>

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -1,5 +1,8 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Text;

View File

@ -6,6 +6,7 @@ public enum AcSerializerType : byte
{
Json = 0,
Binary = 1,
Toon = 2,
}
/// <summary>

View File

@ -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);
}
}

View File

@ -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&lt;string, decimal&gt;").
/// </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;
}
}

View File

@ -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;
}
}

View File

@ -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&lt;T&gt; or List&lt;T&gt; -> 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
}

View File

@ -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
}

View File

@ -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;
}
}
}

View File

@ -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
}

View File

@ -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 &lt;int32&gt;).
/// 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: &lt;string[]&gt; (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 };
}

View File

@ -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);
}
}

View File

@ -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.Trace);
// 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))

1185
ToonExtendedInfo.txt Normal file

File diff suppressed because it is too large Load Diff