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:
parent
223036f8e9
commit
3400cbc65a
|
|
@ -24,7 +24,8 @@
|
|||
"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\\*.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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all types that need documentation (recursive).
|
||||
/// Recursively collects all types that need metadata documentation.
|
||||
/// Uses the same logic as AcBinarySerializer.RegisterMetadataForType.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
// Handle collections
|
||||
var elementType = GetCollectionElementType(underlyingType);
|
||||
if (elementType != null)
|
||||
{
|
||||
CollectTypes(elementType, types);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dictionaries
|
||||
// Handle dictionaries FIRST (before generic IEnumerable check)
|
||||
if (IsDictionaryType(underlyingType, out var keyType, out var valueType))
|
||||
{
|
||||
if (keyType != null) CollectTypes(keyType, types);
|
||||
|
|
@ -132,7 +125,18 @@ public static partial class AcToonSerializer
|
|||
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);
|
||||
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))}";
|
||||
|
||||
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)
|
||||
return "Dictionary mapping keys to values";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
|
||||
using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -28,12 +29,16 @@ public static partial class AcToonSerializer
|
|||
// Get custom attribute if present
|
||||
CustomDescription = type.GetCustomAttribute<ToonDescriptionAttribute>();
|
||||
|
||||
// Check if collection or dictionary
|
||||
// Check if collection or dictionary (matches AcBinarySerializer logic)
|
||||
IsDictionary = IsDictionaryType(type, out _, out _);
|
||||
if (!IsDictionary)
|
||||
{
|
||||
ElementType = GetCollectionElementType(type);
|
||||
IsCollection = ElementType != null;
|
||||
// Only treat as collection if it implements IEnumerable (excluding string)
|
||||
if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
ElementType = GetCollectionElementType(type);
|
||||
IsCollection = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Build property accessors
|
||||
|
|
@ -137,8 +142,13 @@ public static partial class AcToonSerializer
|
|||
return false;
|
||||
}
|
||||
|
||||
// Thread-local visited set to prevent circular reference stack overflow
|
||||
[ThreadStatic]
|
||||
private static HashSet<Type>? t_visitedTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Build human-readable type name for meta section.
|
||||
/// Uses thread-local visited set to prevent stack overflow on circular references.
|
||||
/// </summary>
|
||||
private static string BuildTypeDisplayName(Type type)
|
||||
{
|
||||
|
|
@ -169,30 +179,60 @@ public static partial class AcToonSerializer
|
|||
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)
|
||||
{
|
||||
if (ReferenceEquals(type, GuidType)) return "guid";
|
||||
if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
|
||||
if (ReferenceEquals(type, TimeSpanType)) return "timespan";
|
||||
if (type.IsEnum) return "enum";
|
||||
// Initialize thread-local visited set if needed
|
||||
t_visitedTypes ??= new HashSet<Type>();
|
||||
|
||||
// Check for collections
|
||||
var elementType = GetCollectionElementType(type);
|
||||
if (elementType != null)
|
||||
// Check for circular reference - if already visiting, return simple name
|
||||
if (!t_visitedTypes.Add(type))
|
||||
{
|
||||
var elementTypeName = BuildTypeDisplayName(elementType);
|
||||
return $"{elementTypeName}[]";
|
||||
return type.Name; // Break circular reference
|
||||
}
|
||||
|
||||
// Check for dictionaries
|
||||
if (IsDictionaryType(type, out var keyType, out var valueType))
|
||||
try
|
||||
{
|
||||
var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object";
|
||||
var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object";
|
||||
return $"dict<{keyTypeName}, {valueTypeName}>";
|
||||
}
|
||||
if (ReferenceEquals(type, GuidType)) return "guid";
|
||||
if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
|||
{
|
||||
Mode = ToonSerializationMode.DataOnly,
|
||||
UseMeta = false,
|
||||
UseIndentation = false,
|
||||
UseIndentation = true,
|
||||
OmitDefaultValues = true,
|
||||
WriteTypeNames = false,
|
||||
UseReferenceHandling = false
|
||||
|
|
|
|||
Loading…
Reference in New Issue