#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; namespace imm; /// /// Represents the base interface for versioned, immutable objects. /// Provides access to metadata and potentially the previous version. /// public interface Obj { /// /// Gets the base metadata associated with this version. /// Metadata_Versioned Meta { get; } /// /// Gets the previous version as a base object, if available. /// Returns null if this is the first version or if history is not tracked. /// Obj? Old { get; } /// /// Creates a new version without functional change. /// Returns the new version as an Obj. /// Obj Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ); } /// /// Obj delegate for change notifications. /// public delegate void ChangeDelegate( T? oldVersion, T newVersion ); /// /// Represents a generic interface for immutable objects, /// providing access to basic processing functions and change notifications. /// public interface Obj : Obj where T : Obj { /// /// Gets the change delegate associated with this object. /// [JsonIgnore] ChangeDelegate OnChange { get; set; } /// /// Applies a transformation and creates a new version using basic processing. /// T Process( Func fn, string reason = "Processed", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0, [CallerArgumentExpression( "fn" )] string expStr = "" ); /// /// Creates a new version without a functional change using basic processing. /// Uses 'new' to provide a type-safe return. /// new T Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ); } /* static public class ObjExtensions { /// /// Creates a new version of the object with the specified reason. /// public static T Record( this T obj, string reason = "Recorded" ) where T : Obj { if( obj is Recorded recorded ) { return recorded.Record( reason ); } else if( obj is Versioned versioned ) { return versioned.Record( reason ); } else { // Dont care return obj; } } } */ // --- Metadata Hierarchy --- public interface VersionedMeta { public uint Version { get; } public string Reason { get; } } /// /// Obj metadata for version tracking. /// public record Metadata_Versioned { public uint Version { get; init; } = 1; public string Reason { get; init; } = "Created"; } public interface RecordedMeta : VersionedMeta { public string MemberName { get; } public string FilePath { get; } public int LineNumber { get; } public string Expression { get; } } /// /// Metadata for version and recording (debug/caller info, history). /// public record Metadata_Recorded : Metadata_Versioned, RecordedMeta { internal object? OldObject { get; init; } = null; public string MemberName { get; init; } = ""; public string FilePath { get; init; } = ""; public int LineNumber { get; init; } = 0; public string Expression { get; init; } = ""; } public interface TimedMeta : RecordedMeta { public DateTime CreatedAt { get; } public DateTime TouchedAt { get; } } /// /// Metadata for version, recording, and timing. /// public record Metadata_Timed : Metadata_Recorded, TimedMeta { public DateTime CreatedAt { get; init; } = DateTime.UtcNow; public DateTime TouchedAt { get; init; } = DateTime.UtcNow; } // --- Record Hierarchy --- /// /// Level 1: Basic versioning. Implements Obj. /// public record class Versioned : Obj where T : Versioned { public Metadata_Versioned Meta { get; init; } = new(); [DebuggerBrowsable( DebuggerBrowsableState.Never )] [JsonIgnore] public ChangeDelegate OnChange { get; set; } = ( o, n ) => { }; public virtual Obj? Old => null; Metadata_Versioned Obj.Meta => this.Meta; [JsonIgnore] Obj? Obj.Old => this.Old; public Versioned() { } protected Versioned( Versioned original ) { OnChange = original.OnChange; Meta = original.Meta; } public virtual T Process( Func fn, string reason = "Processed", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0, [CallerArgumentExpression( "fn" )] string expStr = "" ) { var current = (T)this; var next = fn( current ); if( ReferenceEquals( current, next ) ) return current; var newVersion = next with { Meta = new Metadata_Versioned { /*...*/ }, OnChange = current.OnChange }; newVersion.OnChange( current, newVersion ); return newVersion; } /// /// Basic Record. Made virtual. Implements Obj.Record. /// public virtual T Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) => Process( t => t, reason, dbgName, dbgPath, dbgLine ); /// /// Implements Obj.Record by calling the virtual T Record. /// Obj Obj.Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) => this.Record( reason, dbgName, dbgPath, dbgLine ); } /// /// Level 2: Adds history and caller info. /// public record class Recorded : Versioned where T : Recorded { new public Metadata_Recorded Meta { get; init; } = new(); [JsonIgnore] new public T? Old => Meta.OldObject as T; //public override Obj? Old => this.Old; //Metadata_Versioned Obj.Meta => this.Meta; public Recorded() { } protected Recorded( Recorded original ) : base( original ) { Meta = original.Meta; } public override T Process( Func fn, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0, [CallerArgumentExpression( "fn" )] string expStr = "" ) { var current = (T)this; var next = fn( current ); if( ReferenceEquals( current, next ) ) return current; var newMeta = current.Meta with { Version = current.Meta.Version + 1, Reason = reason, MemberName = dbgName, FilePath = dbgPath, LineNumber = dbgLine, Expression = expStr, OldObject = current, }; var newVersion = next with { Meta = newMeta, OnChange = current.OnChange }; newVersion.OnChange( current, newVersion ); return newVersion; } public new T Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) { return Process( t => t, reason, dbgName, dbgPath, dbgLine ); } } /// /// Level 3: Adds timestamps. /// public record class Timed : Recorded where T : Timed { new public Metadata_Timed Meta { get; init; } = new(); //Metadata_Versioned Obj.Meta => this.Meta; public TimeSpan SinceLastTouch => Meta.TouchedAt - ( Old?.Meta as Metadata_Timed )?.TouchedAt ?? TimeSpan.Zero; public TimeSpan TotalAge => Meta.TouchedAt - Meta.CreatedAt; public Timed() { } protected Timed( Timed original ) : base( original ) { Meta = original.Meta; } public override T Process( Func fn, string reason = "", [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0, [CallerArgumentExpression( "fn" )] string dbgExpStr = "" ) { var current = (T)this; var next = fn( current ); if( ReferenceEquals( current, next ) ) return current; /* var newMeta = new Metadata_Timed { Version = current.Meta.Version + 1, Reason = reason, MemberName = dbgMethod, FilePath = dbgPath, LineNumber = dbgLine, Expression = dbgExpression, OldObject = current, CreatedAt = current.Meta.CreatedAt, TouchedAt = DateTime.UtcNow }; */ var newMeta = current.Meta with { Version = current.Meta.Version + 1, Reason = reason, MemberName = dbgMethod, FilePath = dbgPath, LineNumber = dbgLine, Expression = dbgExpStr, OldObject = current, TouchedAt = DateTime.UtcNow }; // Testing: Shouldnt need the OnChange var newVersion = next with { Meta = newMeta }; newVersion.OnChange( current, newVersion ); return newVersion; } public new T Record( string reason = "Recorded", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0 ) { return Process( t => t, reason, dbgName, dbgPath, dbgLine ); } } /* public static class TimedExt { public static T Process( ref T obj, Func fn, string reason = "", [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = 0, [CallerArgumentExpression( "fn" )] string dbgExpression = "" ) where T : imm.Timed { obj = obj.Process( fn, reason, dbgName, dbgPath, dbgLine, dbgExpression ); return obj; } } */