sharplib/srl/srl.Core.cs

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 );
}
}
}