From 4563db15a68e0ae14057a6cb57be65140224e643 Mon Sep 17 00:00:00 2001 From: Marc Hernandez Date: Sun, 17 Aug 2025 10:57:44 -0700 Subject: [PATCH] Ser updates --- db/Processor.cs | 10 ++- ser/CodeGen.cs | 173 +++++++++++++++++++++++++++++++++++++++++++ ser/CodeGenerator.cs | 74 ++++++++++++++++++ ser/XmlSer.cs | 98 ++++++++++++------------ 4 files changed, 304 insertions(+), 51 deletions(-) create mode 100644 ser/CodeGen.cs create mode 100644 ser/CodeGenerator.cs diff --git a/db/Processor.cs b/db/Processor.cs index ee49d97..3490356 100644 --- a/db/Processor.cs +++ b/db/Processor.cs @@ -1,4 +1,10 @@ -using System; +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// S H A R P L I B +// +/// // (c) 2003..2025 + +using System; using System.Collections.Generic; using System.Text; using System.Threading; @@ -95,7 +101,7 @@ namespace db int m_processed = 0; //volatile string ProcessingDebug = ""; - Act m_debugCurrentAct = null; + Act? m_debugCurrentAct = null; diff --git a/ser/CodeGen.cs b/ser/CodeGen.cs new file mode 100644 index 0000000..06f31bf --- /dev/null +++ b/ser/CodeGen.cs @@ -0,0 +1,173 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// S H A R P L I B +// +/// // (c) 2003..2025 + +using System; +using System.IO; +using System.Xml; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using System.Collections.Immutable; +using System.Collections.Concurrent; +using System.Collections.Immutable; + +namespace ser; + +public record CodeGenConfig : imm.Recorded +{ + // Whitelists (if needed, otherwise rely on attributes/defaults) + public ImmutableDictionary> WLProps { get; init; } = ImmutableDictionary>.Empty; + public ImmutableDictionary> WLFields { get; init; } = ImmutableDictionary>.Empty; + + // Default member types to process + public lib.Types TypesDefault { get; init; } = lib.Types.Fields | lib.Types.Props; + + // How to handle backing fields (might be less relevant for code gen) + public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Regular; + + public static CodeGenConfig Default { get; } = new CodeGenConfig(); +} + +public record GenMemberMeta( + MemberInfo Info, + Type Type, + string Name, // Name for code generation (usually original) + bool IsPrimitive, + bool IsCollection, + Type? CollectionElementType, + bool HasDo, + bool HasDont +); + +public record TypeStructureInfo( + Type Type, + List Members, + bool IsValueType, + bool IsCollection +); + +public class TypeStructureAnalyzer +{ + private readonly ConcurrentDictionary _cache = new(); + private readonly CodeGenConfig _cfg; + + public TypeStructureAnalyzer(CodeGenConfig cfg) => _cfg = cfg; + + public TypeStructureInfo Get(Type type) => _cache.GetOrAdd(type, BuildTypeInfo); + + private TypeStructureInfo BuildTypeInfo(Type type) + { + var members = new List(); + var typesTodo = type.GetCustomAttribute(true)?.Types ?? _cfg.TypesDefault; + bool doFields = typesTodo.HasFlag(lib.Types.Fields); + bool doProps = typesTodo.HasFlag(lib.Types.Props); + + // Track processed names to avoid duplicates (e.g., field + prop) + var processedNames = new HashSet(); + + // Process Properties First (often preferred interface) + if (doProps) + { + foreach (var pi in refl.GetAllProperties(type)) + { + if (ProcessMember(pi, false, false, new HashSet(), false, members)) + { + processedNames.Add(pi.Name); + } + } + } + + // Process Fields, avoiding those already covered by properties + if (doFields) + { + foreach (var fi in refl.GetAllFields(type)) + { + var (isBacking, propName) = IsBackingField(fi); + string nameToTest = isBacking ? propName : fi.Name; + + if (!processedNames.Contains(nameToTest)) + { + if(ProcessMember(fi, false, false, new HashSet(), false, members)) + { + processedNames.Add(nameToTest); + } + } + } + } + + return new TypeStructureInfo( + type, + members, + type.IsValueType, + typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string) + ); + } + + private bool ProcessMember(MemberInfo mi, bool filter, bool doImpls, HashSet whitelist, bool isImm, List members) + { + var (hasDo, hasDont, propName) = GetMemberAttributes(mi, out var actualMiForAtts); + + if (hasDont) return false; + if (mi.GetCustomAttribute(true) != null) return false; + if (mi.Name.Contains("k__BackingField") && !propName.Any()) return false; // Skip if backing but no prop found + + string name = string.IsNullOrEmpty(propName) ? mi.Name : propName; + + // Add filtering logic if needed (based on whitelist, etc.) + + var type = (mi is FieldInfo fi) ? fi.FieldType : ((PropertyInfo)mi).PropertyType; + bool isCollection = typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string); + Type? elementType = isCollection ? GetElementType(type) : null; + bool isPrimitive = Type.GetTypeCode(type) != TypeCode.Object && !isCollection; + + members.Add(new GenMemberMeta( + mi, type, name, isPrimitive, isCollection, elementType, hasDo, hasDont + )); + return true; + } + + private (bool, string) IsBackingField(FieldInfo fi) + { + if (fi.Name.StartsWith("<") && fi.Name.EndsWith("BackingField")) + { + var gtIndex = fi.Name.IndexOf('>'); + if (gtIndex > 1) { + return (true, fi.Name.Substring(1, gtIndex - 1)); + } + } + return (false, ""); + } + + private (bool hasDo, bool hasDont, string propName) GetMemberAttributes(MemberInfo mi, out MemberInfo actualMi) + { + actualMi = mi; + string propName = ""; + if (mi is FieldInfo fi && IsBackingField(fi).Item1) + { + propName = IsBackingField(fi).Item2; + var propInfo = mi.DeclaringType?.GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (propInfo != null) actualMi = propInfo; + } else if (mi is PropertyInfo) { + propName = mi.Name; + } + + return ( + actualMi.GetCustomAttribute() != null, + actualMi.GetCustomAttribute() != null, + propName + ); + } + + private Type GetElementType(Type collectionType) + { + if (collectionType.IsArray) return collectionType.GetElementType()!; + if (collectionType.IsGenericType) return collectionType.GetGenericArguments().Last(); // Usually last (e.g., List, Dict) + return typeof(object); // Fallback + } + + // Add GetFilters and FilterField if needed, or simplify as above +} \ No newline at end of file diff --git a/ser/CodeGenerator.cs b/ser/CodeGenerator.cs new file mode 100644 index 0000000..ca25959 --- /dev/null +++ b/ser/CodeGenerator.cs @@ -0,0 +1,74 @@ +using System.Text; +using System.Collections.Generic; +using System; +using System.Linq; + +namespace ser; + +public abstract class CodeGenerator +{ + protected StringBuilder _sb = new StringBuilder(); + protected int _indent = 0; + protected TypeStructureAnalyzer _analyzer; + protected CodeGenConfig _config; + protected HashSet _generatedTypes = new(); // Track to avoid re-generating + + public CodeGenerator( CodeGenConfig config ) + { + _config = config; + _analyzer = new TypeStructureAnalyzer( config ); + } + + // Main entry point + public string Generate( Type type, string ns = "GeneratedCode" ) + { + _sb.Clear(); + WriteLine( "using System;" ); + WriteLine( "using System.Collections.Generic;" ); + WriteLine( "using System.Linq;" ); + WriteLine( "" ); + WriteLine( $"namespace {ns};" ); + WriteLine( "" ); + GenerateForType( type ); + return _sb.ToString(); + } + + // Core generation logic - needs to be recursive for dependencies + protected virtual void GenerateForType( Type type ) + { + if( type == null || !CanGenerateFor( type ) || _generatedTypes.Contains( type ) ) + return; + + _generatedTypes.Add( type ); + var info = _analyzer.Get( type ); + + // Generate dependencies first + foreach( var member in info.Members ) + { + GenerateForType( member.Type ); + if( member.IsCollection && member.CollectionElementType != null ) + { + GenerateForType( member.CollectionElementType ); + } + } + + // Generate the actual code + GenerateClassHeader( info ); + BeginBlock(); + GenerateClassBody( info ); + EndBlock(); + } + + // Abstract methods to be implemented by specific generators + protected abstract void GenerateClassHeader( TypeStructureInfo info ); + protected abstract void GenerateClassBody( TypeStructureInfo info ); + protected abstract bool CanGenerateFor( Type type ); // Check if we should generate for this type + + // Helper methods + protected void WriteLine( string line = "" ) => _sb.AppendLine( new string( '\t', _indent ) + line ); + protected void BeginBlock() { WriteLine( "{" ); _indent++; } + protected void EndBlock() { _indent--; WriteLine( "}" ); } + protected string GetTypeName( Type t ) => t.IsGenericType + ? $"{t.Name.Split( '`' )[0]}<{string.Join( ", ", t.GetGenericArguments().Select( GetTypeName ) )}>" + : t.Name; // Basic handling, needs improvement for full names/namespaces +} \ No newline at end of file diff --git a/ser/XmlSer.cs b/ser/XmlSer.cs index 0c71ad6..273b99a 100644 --- a/ser/XmlSer.cs +++ b/ser/XmlSer.cs @@ -13,7 +13,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Linq.Expressions; -namespace serdes; +namespace ser; #region Attributes & Enums (Mostly unchanged, ensure these exist) @@ -24,7 +24,7 @@ namespace serdes; public enum Datastructure { Tree, Graph } public enum BackingFieldNaming { Short, Regular } public enum POD { Attributes, Elements } -public record struct TypeProxy( Func ser, Func des ); +public record struct TypeProxy( Func fnSer, Func fnDes ); public record XmlCfg : imm.Recorded { @@ -303,8 +303,8 @@ public class TypeResolver public interface ITypeHandler { bool CanHandle( TypeSerializationInfo typeInfo, XmlElement? elem = null ); // Elem needed for Deser - void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ); - object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ); + void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ); + object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ); } // --- Primitive Handler --- @@ -312,15 +312,15 @@ public class PrimitiveHandler : ITypeHandler { public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => Type.GetTypeCode( ti.Type ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( ti.Type ); - public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) { string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText; if( val == "null" ) return null; - return ser._resolver.ConvertSimple( val, expectedType ); + return xml._resolver.ConvertSimple( val, expectedType ); } - public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) { @@ -330,14 +330,14 @@ public class PrimitiveHandler : ITypeHandler return; } - bool writeElements = ser._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter ); + bool writeElements = xml._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter ); if( !writeElements && writer is XmlTextWriter tw ) writeElements = tw.WriteState != WriteState.Element; if( writeElements ) writer.WriteStartElement( name ); - if( forceType || ser._cfg.POD == POD.Elements ) + if( forceType || xml._cfg.POD == POD.Elements ) { if( forceType ) writer.WriteAttributeString( "_.t", obj.GetType().FullName ); @@ -359,29 +359,29 @@ public class ProxyHandler : ITypeHandler public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsProxy || ( elem?.HasAttribute( "proxy" ) ?? false ); - public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) { - var ti = ser._meta.Get( expectedType ); // Re-get to ensure we have proxy info + var ti = xml._meta.Get( expectedType ); // Re-get to ensure we have proxy info if( !elem.HasAttribute( "proxy" ) || !ti.ProxyDef.HasValue ) { log.warn( $"Proxy read failed for {expectedType.Name}. Fallback needed." ); return null; // Should fall back or throw } var proxyVal = elem.GetAttribute( "proxy" ); - return ti.ProxyDef.Value.des( expectedType.FullName, proxyVal ); + return ti.ProxyDef.Value.fnDes( expectedType.FullName, proxyVal ); } - public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) - { ser.GetHandler( typeof( object ) ).WriteXml( ser, writer, null, name, memberType, forceType ); return; } + { xml.GetHandler( typeof( object ) ).WriteXml( xml, writer, null, name, memberType, forceType ); return; } - var ti = ser._meta.Get( obj.GetType() ); + var ti = xml._meta.Get( obj.GetType() ); if( !ti.ProxyDef.HasValue ) { log.error( "Proxy write called without proxy def!" ); return; } writer.WriteStartElement( name ); - var proxyStr = ti.ProxyDef.Value.ser( obj ); + var proxyStr = ti.ProxyDef.Value.fnSer( obj ); writer.WriteAttributeString( "proxy", proxyStr ); writer.WriteEndElement(); } @@ -392,11 +392,11 @@ public class ISerializableHandler : ITypeHandler { public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => ti.IsISerializable; - public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) { // Create/Get instance (needs FormatterServices for ISerializable) object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType ); - long id = ser.TrackIfGraph( obj, elem ); // Track it + long id = xml.TrackIfGraph( obj, elem ); // Track it var serInfo = new SerializationInfo( expectedType, new FormatterConverter() ); @@ -405,16 +405,16 @@ public class ISerializableHandler : ITypeHandler if( objNode is XmlElement childElem ) { string childName = childElem.Name; - Type? childType = ser._resolver.FindType( childElem.GetAttribute( "_.t" ) ); + Type? childType = xml._resolver.FindType( childElem.GetAttribute( "_.t" ) ); if( childType != null ) { - var desValue = ser.ReadNode( childElem, childType, null ); + var desValue = xml.ReadNode( childElem, childType, null ); serInfo.AddValue( childName, desValue, childType ); } } } - var context = new StreamingContext( StreamingContextStates.All ); // Or use ser.Context + var context = new StreamingContext( StreamingContextStates.All ); // Or use xml.Context var cons = expectedType.GetConstructor( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof( SerializationInfo ), typeof( StreamingContext ) }, null ); @@ -434,7 +434,7 @@ public class ISerializableHandler : ITypeHandler return obj; } - public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) { /* Write null */ return; } @@ -442,9 +442,9 @@ public class ISerializableHandler : ITypeHandler { /* Error */ return; } writer.WriteStartElement( name ); - ser.WriteTypeAttr( writer, memberType, obj.GetType() ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); - if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) { if( first ) { @@ -454,7 +454,7 @@ public class ISerializableHandler : ITypeHandler foreach( var member in serInfo ) { - ser.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer + xml.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer } } } @@ -468,7 +468,7 @@ public class CollectionHandler : ITypeHandler public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => typeof( IEnumerable ).IsAssignableFrom( ti.Type ) && ti.Type != typeof( string ); - public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) { // Determine element type Type elemType = GetElementType( expectedType ); @@ -477,14 +477,14 @@ public class CollectionHandler : ITypeHandler var listType = typeof( List<> ).MakeGenericType( elemType ); var list = (IList)Activator.CreateInstance( listType )!; - ser.TrackIfGraph( list, elem ); // Track list if graph + xml.TrackIfGraph( list, elem ); // Track list if graph // Populate the list foreach( XmlNode node in elem.ChildNodes ) { if( node is XmlElement childElem ) { - list.Add( ser.ReadNode( childElem, elemType, null ) ); + list.Add( xml.ReadNode( childElem, elemType, null ) ); } } @@ -492,7 +492,7 @@ public class CollectionHandler : ITypeHandler return ConvertToFinalCollection( list, expectedType, elemType ); } - public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) { /* Write null */ return; } @@ -500,9 +500,9 @@ public class CollectionHandler : ITypeHandler { /* Error */ return; } writer.WriteStartElement( name ); - ser.WriteTypeAttr( writer, memberType, obj.GetType() ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); - if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) { if( first ) { @@ -510,7 +510,7 @@ public class CollectionHandler : ITypeHandler int i = 0; foreach( var item in collection ) { - ser.WriteNode( writer, item, $"i{i++}", elemType, false ); + xml.WriteNode( writer, item, $"i{i++}", elemType, false ); } } } @@ -569,26 +569,26 @@ public class ObjectHandler : ITypeHandler { public bool CanHandle( TypeSerializationInfo ti, XmlElement? elem ) => true; // Fallback - public object? ReadXml( XmlSer ser, XmlElement elem, Type expectedType, object? existing ) + public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing ) { - var actualType = ser._resolver.Resolve( elem, expectedType ); - var ti = ser._meta.Get( actualType ); + var actualType = xml._resolver.Resolve( elem, expectedType ); + var ti = xml._meta.Get( actualType ); // 1. Get/Create Instance - var (obj, _) = GetOrCreateInstance( ser, elem, actualType, existing ); + var (obj, _) = GetOrCreateInstance( xml, elem, actualType, existing ); if( obj == null ) return null; // Handle graph refs (if already processed) - if( ser._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) + if( xml._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) ) { long id = long.Parse( elem.GetAttribute( "ref" ) ); - if( ser._processed.TryGetValue( id, out var processedObj ) ) + if( xml._processed.TryGetValue( id, out var processedObj ) ) return processedObj; } // Track if it's new - ser.TrackIfGraph( obj, elem ); + xml.TrackIfGraph( obj, elem ); // 2. Hydrate foreach( var memberMeta in ti.Members ) @@ -602,11 +602,11 @@ public class ObjectHandler : ITypeHandler if( isAttribute ) { - memberValue = ser._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type ); + memberValue = xml._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type ); } else // Child Element { - memberValue = ser.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue ); + memberValue = xml.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue ); } // Set value, respecting lib.Do/lib.Dont and pre-hydration @@ -626,16 +626,16 @@ public class ObjectHandler : ITypeHandler return obj; } - public void WriteXml( XmlSer ser, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) + public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType ) { if( obj == null ) { /* Write null */ return; } writer.WriteStartElement( name ); - ser.WriteTypeAttr( writer, memberType, obj.GetType() ); - var ti = ser._meta.Get( obj.GetType() ); + xml.WriteTypeAttr( writer, memberType, obj.GetType() ); + var ti = xml._meta.Get( obj.GetType() ); - if( ser.HandleGraphWrite( writer, obj, out bool first ) ) + if( xml.HandleGraphWrite( writer, obj, out bool first ) ) { if( first ) { @@ -651,7 +651,7 @@ public class ObjectHandler : ITypeHandler } else // Else, write element { - ser.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false ); + xml.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false ); } } } @@ -697,7 +697,7 @@ public class ObjectHandler : ITypeHandler } - private (object? obj, long id) GetOrCreateInstance( XmlSer ser, XmlElement elem, Type type, object? existing ) + private (object? obj, long id) GetOrCreateInstance( XmlSer xml, XmlElement elem, Type type, object? existing ) { long id = -1; bool first = true; @@ -705,7 +705,7 @@ public class ObjectHandler : ITypeHandler // Check existing if( existing != null && type.IsAssignableFrom( existing.GetType() ) ) { - id = ser._idGen.GetId( existing, out first ); + id = xml._idGen.GetId( existing, out first ); return (existing, id); } @@ -728,7 +728,7 @@ public class ObjectHandler : ITypeHandler return (null, -1); } - id = ser._idGen.GetId( newObj, out first ); + id = xml._idGen.GetId( newObj, out first ); return (newObj, id); } }