376 lines
8.6 KiB
C#
376 lines
8.6 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
|
|
namespace srl;
|
|
|
|
// --- INTERFACES ---
|
|
|
|
public interface Driver
|
|
{
|
|
// Structural
|
|
void BeginScope( string name, Type type, int id );
|
|
void EndScope();
|
|
void BeginCollection( string name, int count );
|
|
void EndCollection();
|
|
|
|
// Data
|
|
void WriteAttr( string name, string value ); // Primitives & Compact Strings
|
|
void WriteRef( string name, int id ); // DAG/Cycle Reference
|
|
|
|
// Metadata Hook
|
|
void OnProp( MemberInfo member, string name );
|
|
}
|
|
|
|
public interface IInput
|
|
{
|
|
string? Value { get; }
|
|
bool IsLeaf { get; }
|
|
IInput? GetAttr( string name );
|
|
IInput? GetChild( string name );
|
|
}
|
|
|
|
// --- THE MODEL (Introspection) ---
|
|
|
|
public class TypePlan
|
|
{
|
|
public string Name;
|
|
public bool IsCollection;
|
|
public bool IsHybrid; // True if Type has a StringParser registered
|
|
public StringParser Parser; // The "Compact" converter
|
|
public List<PropPlan> Props = new();
|
|
}
|
|
|
|
public class PropPlan
|
|
{
|
|
public string Name;
|
|
public string MemberName;
|
|
public Type Type;
|
|
public bool IsSmart; // Primitive/Enum/String
|
|
public MemberInfo Info;
|
|
|
|
// Fast Accessors
|
|
public Func<object, object?> Getter;
|
|
public Action<object, object?> Setter;
|
|
}
|
|
|
|
public static class Model
|
|
{
|
|
private static Dictionary<Type, TypePlan> _cache = new();
|
|
|
|
public static TypePlan Get( Type t )
|
|
{
|
|
if( _cache.TryGetValue( t, out var plan ) )
|
|
return plan;
|
|
|
|
plan = new TypePlan { Name = t.Name };
|
|
|
|
// 1. Check for Custom Parsers (Hybrid Mode)
|
|
if( Parsers.TryGet( t, out var parser ) )
|
|
{
|
|
plan.IsHybrid = true;
|
|
plan.Parser = parser;
|
|
}
|
|
|
|
// 2. Check for Collections
|
|
if( t != typeof( string ) && typeof( IEnumerable ).IsAssignableFrom( t ) )
|
|
{
|
|
plan.IsCollection = true;
|
|
_cache[t] = plan;
|
|
return plan;
|
|
}
|
|
|
|
// 3. Scan Properties
|
|
foreach( var p in t.GetProperties( BindingFlags.Public | BindingFlags.Instance ) )
|
|
{
|
|
if( Attribute.IsDefined( p, typeof( IgnoreAttribute ) ) )
|
|
continue;
|
|
|
|
var prop = new PropPlan
|
|
{
|
|
Name = p.Name, // Default to PascalCase, Drivers can lower if needed
|
|
MemberName = p.Name,
|
|
Type = p.PropertyType,
|
|
Info = p,
|
|
Getter = ( o ) => p.GetValue( o ),
|
|
Setter = ( o, v ) => p.SetValue( o, v )
|
|
};
|
|
|
|
// Override name
|
|
var nameAttr = p.GetCustomAttribute<NameAttribute>();
|
|
if( nameAttr != null )
|
|
prop.Name = nameAttr.Name;
|
|
|
|
// Is "Smart" (Atomic)?
|
|
prop.IsSmart = prop.Type.IsPrimitive || prop.Type.IsEnum ||
|
|
prop.Type == typeof( string ) || prop.Type == typeof( Guid );
|
|
|
|
plan.Props.Add( prop );
|
|
}
|
|
|
|
_cache[t] = plan;
|
|
return plan;
|
|
}
|
|
}
|
|
|
|
// --- ATTRIBUTES ---
|
|
public class IgnoreAttribute : Attribute { }
|
|
public class NameAttribute : Attribute { public string Name; public NameAttribute( string n ) => Name = n; }
|
|
|
|
// --- UTILITIES ---
|
|
|
|
public struct StringParser
|
|
{
|
|
public Func<object, string> To;
|
|
public Func<string, object> From;
|
|
}
|
|
|
|
public static class Parsers
|
|
{
|
|
private static Dictionary<Type, StringParser> _registry = new();
|
|
|
|
public static void Register<T>( Func<T, string> to, Func<string, T> from )
|
|
{
|
|
_registry[typeof( T )] = new StringParser { To = o => to( (T)o ), From = s => from( s ) };
|
|
}
|
|
|
|
public static bool TryGet( Type t, out StringParser p )
|
|
{
|
|
if( _registry.TryGetValue( t, out p ) )
|
|
return true;
|
|
// Auto-Discovery could go here (Static Parse methods)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static class Binder
|
|
{
|
|
// Handles "Cool.Rare" dot notation
|
|
public static void Apply( object root, string path, string value )
|
|
{
|
|
var current = root;
|
|
var parts = path.Split( '.' );
|
|
|
|
for( int i = 0; i < parts.Length; i++ )
|
|
{
|
|
var part = parts[i];
|
|
var isLast = i == parts.Length - 1;
|
|
var plan = Model.Get( current.GetType() );
|
|
|
|
// Case-Insensitive Match
|
|
var prop = plan.Props.Find( p => p.Name.Equals( part, StringComparison.OrdinalIgnoreCase ) );
|
|
if( prop == null )
|
|
return;
|
|
|
|
if( isLast )
|
|
{
|
|
var val = ParseUtils.Convert( value, prop.Type );
|
|
prop.Setter( current, val );
|
|
}
|
|
else
|
|
{
|
|
var next = prop.Getter( current );
|
|
if( next == null )
|
|
{
|
|
next = Activator.CreateInstance( prop.Type );
|
|
prop.Setter( current, next );
|
|
}
|
|
current = next;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class ParseUtils
|
|
{
|
|
public static object Convert( string raw, Type t )
|
|
{
|
|
if( t == typeof( string ) )
|
|
return raw;
|
|
if( t == typeof( int ) )
|
|
return int.Parse( raw );
|
|
if( t == typeof( float ) )
|
|
return float.Parse( raw.Replace( "f", "" ) );
|
|
if( t == typeof( bool ) )
|
|
return bool.Parse( raw );
|
|
if( t.IsEnum )
|
|
return Enum.Parse( t, raw );
|
|
// Fallback to TypeConverter
|
|
var cv = TypeDescriptor.GetConverter( t );
|
|
if( cv != null && cv.CanConvertFrom( typeof( string ) ) )
|
|
return cv.ConvertFrom( raw );
|
|
return null;
|
|
}
|
|
|
|
public static void CopyFields( object src, object dst )
|
|
{
|
|
foreach( var p in src.GetType().GetProperties() )
|
|
if( p.CanRead && p.CanWrite )
|
|
p.SetValue( dst, p.GetValue( src ) );
|
|
}
|
|
}
|
|
|
|
// --- THE ENGINE (Walker & Loader) ---
|
|
|
|
public static class Walker
|
|
{
|
|
public class Context
|
|
{
|
|
private Dictionary<object, int> _seen = new( ReferenceEqualityComparer.Instance );
|
|
private int _nextId = 1;
|
|
public (int, bool) GetId( object o )
|
|
{
|
|
if( _seen.TryGetValue( o, out var id ) )
|
|
return (id, false);
|
|
_seen[o] = _nextId;
|
|
return (_nextId++, true);
|
|
}
|
|
}
|
|
|
|
public static void Serialize( object root, Driver d )
|
|
{
|
|
if( root == null )
|
|
return;
|
|
SerializeRecursive( root, d, new Context(), "root", null );
|
|
}
|
|
|
|
private static void SerializeRecursive( object obj, Driver d, Context ctx, string name, MemberInfo? member )
|
|
{
|
|
if( obj == null )
|
|
return;
|
|
|
|
Type type = obj.GetType();
|
|
var plan = Model.Get( type );
|
|
|
|
// STRATEGY 1: COMPACT (Hybrid Parser)
|
|
if( plan.IsHybrid )
|
|
{
|
|
if( member != null )
|
|
d.OnProp( member, name );
|
|
d.WriteAttr( name, plan.Parser.To( obj ) );
|
|
return;
|
|
}
|
|
|
|
// STRATEGY 2: DAG CHECK
|
|
bool isRef = !type.IsValueType && type != typeof( string );
|
|
if( isRef )
|
|
{
|
|
var (id, isNew) = ctx.GetId( obj );
|
|
if( !isNew )
|
|
{ d.WriteRef( name, id ); return; }
|
|
if( member != null )
|
|
d.OnProp( member, name );
|
|
d.BeginScope( name, type, id );
|
|
}
|
|
else
|
|
{
|
|
if( member != null )
|
|
d.OnProp( member, name );
|
|
d.BeginScope( name, type, 0 );
|
|
}
|
|
|
|
// STRATEGY 3: COLLECTIONS
|
|
if( plan.IsCollection )
|
|
{
|
|
var list = (IEnumerable)obj;
|
|
int count = 0; // Simple count (could optimize for ICollection)
|
|
foreach( var _ in list )
|
|
count++;
|
|
|
|
d.BeginCollection( name, count );
|
|
foreach( var item in list )
|
|
SerializeRecursive( item, d, ctx, "item", null );
|
|
d.EndCollection();
|
|
}
|
|
else
|
|
{
|
|
// STRATEGY 4: STANDARD OBJECT
|
|
foreach( var prop in plan.Props )
|
|
{
|
|
var val = prop.Getter( obj );
|
|
if( prop.IsSmart )
|
|
{
|
|
d.OnProp( prop.Info, prop.Name );
|
|
d.WriteAttr( prop.Name, val?.ToString() ?? "" );
|
|
}
|
|
else
|
|
{
|
|
if( val != null )
|
|
SerializeRecursive( val, d, ctx, prop.Name, prop.Info );
|
|
}
|
|
}
|
|
}
|
|
d.EndScope();
|
|
}
|
|
}
|
|
|
|
public static class Loader
|
|
{
|
|
public static void Load( object target, IInput input )
|
|
{
|
|
if( target == null || input == null )
|
|
return;
|
|
var plan = Model.Get( target.GetType() );
|
|
|
|
// 1. HYBRID PARSE (Compact String)
|
|
// If we have a parser AND input is just a value "1,1"
|
|
if( plan.IsHybrid && input.IsLeaf && !string.IsNullOrWhiteSpace( input.Value ) )
|
|
{
|
|
try
|
|
{
|
|
var newObj = plan.Parser.From( input.Value );
|
|
ParseUtils.CopyFields( newObj, target );
|
|
return;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
// 2. STRUCTURAL MAP
|
|
foreach( var prop in plan.Props )
|
|
{
|
|
// Look for Attribute OR Child Element
|
|
var sub = input.GetAttr( prop.Name ) ?? input.GetChild( prop.Name );
|
|
|
|
// Look for Dot Notation (e.g. "Pos.X") in attributes
|
|
// (Note: This simple loop doesn't scan ALL attrs for dots,
|
|
// it relies on the caller or specific recursive logic.
|
|
// For full dot support on root, we need to iterate input attributes if possible.
|
|
// But 'Binder' below handles it if we pass the specific attr key).
|
|
|
|
if( sub != null )
|
|
{
|
|
if( prop.IsSmart )
|
|
{
|
|
if( sub.Value != null )
|
|
prop.Setter( target, ParseUtils.Convert( sub.Value, prop.Type ) );
|
|
}
|
|
else
|
|
{
|
|
var child = prop.Getter( target );
|
|
if( child == null )
|
|
{
|
|
child = Activator.CreateInstance( prop.Type );
|
|
prop.Setter( target, child );
|
|
}
|
|
Load( child, sub );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper to scan all attributes on an element for "Cool.Rare" patterns
|
|
public static void LoadDotNotations( object target, IEnumerable<(string k, string v)> attrs )
|
|
{
|
|
foreach( var (k, v) in attrs )
|
|
{
|
|
if( k.Contains( '.' ) )
|
|
Binder.Apply( target, k, v );
|
|
}
|
|
}
|
|
}
|
|
|