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