Improve AcToonSerializer type metadata handling & tests

- Refined type metadata serialization: collections and dictionaries are now detected and described more accurately, avoiding generic type names (e.g., List`1) and redundant "object" element types.
- Added circular reference protection to type name generation to prevent stack overflows and duplicate type names.
- Updated AcToonSerializerOptions.Compact to use indentation for better readability.
- Introduced ToonTests with unit tests to ensure type metadata correctness, uniqueness, and clarity.
- Added AyCode.Core project to the solution and adjusted namespaces/usings for consistency.
This commit is contained in:
Loretta 2026-01-12 08:36:23 +01:00
parent 223036f8e9
commit 3400cbc65a
5 changed files with 83 additions and 35 deletions

View File

@ -24,7 +24,8 @@
"Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")", "Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")", "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")",
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")" "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")",
"Bash(timeout 30 dotnet run:*)"
] ]
} }
} }

View File

@ -106,7 +106,8 @@ public static partial class AcToonSerializer
} }
/// <summary> /// <summary>
/// Collect all types that need documentation (recursive). /// Recursively collects all types that need metadata documentation.
/// Uses the same logic as AcBinarySerializer.RegisterMetadataForType.
/// </summary> /// </summary>
private static void CollectTypes(Type type, HashSet<Type> types) private static void CollectTypes(Type type, HashSet<Type> types)
{ {
@ -116,15 +117,7 @@ public static partial class AcToonSerializer
if (!types.Add(underlyingType)) return; // Already processed if (!types.Add(underlyingType)) return; // Already processed
// Handle collections // Handle dictionaries FIRST (before generic IEnumerable check)
var elementType = GetCollectionElementType(underlyingType);
if (elementType != null)
{
CollectTypes(elementType, types);
return;
}
// Handle dictionaries
if (IsDictionaryType(underlyingType, out var keyType, out var valueType)) if (IsDictionaryType(underlyingType, out var keyType, out var valueType))
{ {
if (keyType != null) CollectTypes(keyType, types); if (keyType != null) CollectTypes(keyType, types);
@ -132,7 +125,18 @@ public static partial class AcToonSerializer
return; return;
} }
// Handle object properties // Handle collections/arrays (matches AcBinarySerializer logic)
if (typeof(IEnumerable).IsAssignableFrom(underlyingType) && !ReferenceEquals(underlyingType, StringType))
{
var elementType = GetCollectionElementType(underlyingType);
if (elementType != null)
{
CollectTypes(elementType, types);
}
return;
}
// Handle object properties (traverse type graph)
var metadata = GetTypeMetadata(underlyingType); var metadata = GetTypeMetadata(underlyingType);
foreach (var prop in metadata.Properties) foreach (var prop in metadata.Properties)
{ {
@ -395,8 +399,11 @@ public static partial class AcToonSerializer
return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}"; return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}";
var metadata = GetTypeMetadata(type); var metadata = GetTypeMetadata(type);
if (metadata.IsCollection)
return $"Collection of {metadata.ElementType?.Name ?? "items"}"; // Only treat as collection if ElementType is not System.Object (fallback value from GetCollectionElementType)
if (metadata.IsCollection && metadata.ElementType != null && metadata.ElementType != typeof(object))
return $"Collection of {metadata.ElementType.Name}";
if (metadata.IsDictionary) if (metadata.IsDictionary)
return "Dictionary mapping keys to values"; return "Dictionary mapping keys to values";

View File

@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer; using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
namespace AyCode.Core.Serializers.Toons; namespace AyCode.Core.Serializers.Toons;

View File

@ -1,3 +1,4 @@
using System.Collections;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -28,12 +29,16 @@ public static partial class AcToonSerializer
// Get custom attribute if present // Get custom attribute if present
CustomDescription = type.GetCustomAttribute<ToonDescriptionAttribute>(); CustomDescription = type.GetCustomAttribute<ToonDescriptionAttribute>();
// Check if collection or dictionary // Check if collection or dictionary (matches AcBinarySerializer logic)
IsDictionary = IsDictionaryType(type, out _, out _); IsDictionary = IsDictionaryType(type, out _, out _);
if (!IsDictionary) if (!IsDictionary)
{ {
ElementType = GetCollectionElementType(type); // Only treat as collection if it implements IEnumerable (excluding string)
IsCollection = ElementType != null; if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
{
ElementType = GetCollectionElementType(type);
IsCollection = true;
}
} }
// Build property accessors // Build property accessors
@ -137,8 +142,13 @@ public static partial class AcToonSerializer
return false; return false;
} }
// Thread-local visited set to prevent circular reference stack overflow
[ThreadStatic]
private static HashSet<Type>? t_visitedTypes;
/// <summary> /// <summary>
/// Build human-readable type name for meta section. /// Build human-readable type name for meta section.
/// Uses thread-local visited set to prevent stack overflow on circular references.
/// </summary> /// </summary>
private static string BuildTypeDisplayName(Type type) private static string BuildTypeDisplayName(Type type)
{ {
@ -169,30 +179,60 @@ public static partial class AcToonSerializer
return isNullable ? baseName + "?" : baseName; return isNullable ? baseName + "?" : baseName;
} }
/// <summary>
/// Get complex type name WITH circular reference protection using thread-local visited set.
/// </summary>
private static string GetComplexTypeName(Type type) private static string GetComplexTypeName(Type type)
{ {
if (ReferenceEquals(type, GuidType)) return "guid"; // Initialize thread-local visited set if needed
if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset"; t_visitedTypes ??= new HashSet<Type>();
if (ReferenceEquals(type, TimeSpanType)) return "timespan";
if (type.IsEnum) return "enum";
// Check for collections // Check for circular reference - if already visiting, return simple name
var elementType = GetCollectionElementType(type); if (!t_visitedTypes.Add(type))
if (elementType != null)
{ {
var elementTypeName = BuildTypeDisplayName(elementType); return type.Name; // Break circular reference
return $"{elementTypeName}[]";
} }
// Check for dictionaries try
if (IsDictionaryType(type, out var keyType, out var valueType))
{ {
var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object"; if (ReferenceEquals(type, GuidType)) return "guid";
var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object"; if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
return $"dict<{keyTypeName}, {valueTypeName}>"; if (ReferenceEquals(type, TimeSpanType)) return "timespan";
} if (type.IsEnum) return "enum";
return type.Name; // For collections: recursively build element type name (safe with visited tracking)
if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
{
var elementType = GetCollectionElementType(type);
if (elementType != null && elementType != typeof(object))
{
var elementTypeName = BuildTypeDisplayName(elementType);
return $"{elementTypeName}[]";
}
}
// For dictionaries: recursively build key/value type names (safe with visited tracking)
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}>";
}
// Simple type name for complex objects
return type.Name;
}
finally
{
// Remove from visited set when done (allows reuse in different branches)
t_visitedTypes.Remove(type);
// Clear the set if empty to avoid memory leaks
if (t_visitedTypes.Count == 0)
{
t_visitedTypes = null;
}
}
} }
} }
} }

View File

@ -181,7 +181,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
{ {
Mode = ToonSerializationMode.DataOnly, Mode = ToonSerializationMode.DataOnly,
UseMeta = false, UseMeta = false,
UseIndentation = false, UseIndentation = true,
OmitDefaultValues = true, OmitDefaultValues = true,
WriteTypeNames = false, WriteTypeNames = false,
UseReferenceHandling = false UseReferenceHandling = false