Add Toon serializer: LLM-optimized format & rich metadata

Introduces Toon (Token-Oriented Object Notation), a new serialization format designed for LLM readability and token efficiency. Adds core Toon serializer, options, attribute system (ToonDescriptionAttribute), and comprehensive documentation. Supports explicit type metadata, smart fallback/placeholder logic, multi-turn workflows, reference handling, and multi-line strings. No breaking changes; Toon is opt-in and complements existing JSON/binary serializers.
This commit is contained in:
Loretta 2026-01-10 20:13:54 +01:00
parent 028c80db94
commit bbb21dbb67
16 changed files with 3762 additions and 9 deletions

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

@ -18,4 +18,5 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

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

@ -200,31 +200,35 @@ public abstract class AcLoggerBase : IAcLoggerBase
if (string.IsNullOrEmpty(message) && exception == null)
return;
var fullMessage = eventId.Id != 0
? $"[{eventId.Id}:{eventId.Name}] {message}"
: message;
//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);
Detail(fullMessage, category, memberName);
break;
case MsLogLevel.Debug:
Debug(fullMessage, category);
Debug(fullMessage, category, memberName);
break;
case MsLogLevel.Information:
Info(fullMessage, category);
Info(fullMessage, category, memberName);
break;
case MsLogLevel.Warning:
Warning(fullMessage, category);
Warning(fullMessage, category, memberName);
break;
case MsLogLevel.Error:
Error(fullMessage, exception, category);
Error(fullMessage, exception, category, memberName);
break;
case MsLogLevel.Critical:
Error($"[CRITICAL] {fullMessage}", exception, category);
Error($"[CRITICAL] {fullMessage}", exception, category, memberName);
break;
case MsLogLevel.None:
default:

View File

@ -1,7 +1,10 @@
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

View File

@ -4,6 +4,7 @@ public enum AcSerializerType : byte
{
Json = 0,
Binary = 1,
Toon = 2,
}
public abstract class AcSerializerOptions

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,420 @@
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)
{
var enumValue = Convert.ToInt32(value);
var enumName = Enum.GetName(underlyingType, value);
valueStr = enumName != null ? $"\"{enumName}\"" : enumValue.ToString(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);
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
if (context.Options.ShowCollectionCount)
{
context.Write($"<dict> (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("}");
}
}

View File

@ -0,0 +1,504 @@
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.
/// </summary>
private static void WriteMetaSection(Type type, ToonSerializationContext context)
{
if (!context.Options.UseMeta) return;
// @meta header
context.WriteLine("@meta {");
context.CurrentIndentLevel++;
context.WriteProperty("version", $"\"{FormatVersion}\"");
context.WriteProperty("format", "\"toon\"");
// Collect all types that need metadata
var typesToDocument = new HashSet<Type>();
CollectTypes(type, typesToDocument);
// Write type list
context.WriteIndent();
context.Write("types = [");
var first = true;
foreach (var t in typesToDocument)
{
if (!first) context.Write(", ");
context.Write($"\"{t.Name}\"");
first = false;
}
context.WriteLine("]");
context.CurrentIndentLevel--;
context.WriteLine("}");
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)
{
var metadata = GetTypeMetadata(type);
// Type description with fallback chain and placeholder resolution
var typeDescription = GetFinalTypeDescription(type, metadata);
context.WriteIndentedLine($"{type.Name}: \"{typeDescription}\"");
// Type-level purpose (if enhanced metadata is enabled)
if (context.Options.UseEnhancedMetadata)
{
var typePurpose = GetFinalTypePurpose(type, metadata);
if (!string.IsNullOrEmpty(typePurpose))
{
context.CurrentIndentLevel++;
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);
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}\"");
context.CurrentIndentLevel--;
}
else
{
// Simple format
context.WriteIndentedLine($"{prop.Name}: {typeHint} \"{propDescription}\"");
}
}
context.CurrentIndentLevel--;
context.WriteLine();
}
/// <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);
}
}

View File

@ -0,0 +1,235 @@
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);
_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,291 @@
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);
}
}
#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,239 @@
namespace AyCode.Core.Serializers.Toons;
/// <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>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>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>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>
/// 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