sharplib/imm/Imm.cs

359 lines
10 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
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>
[JsonIgnore]
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;
}
}