Files
MycLib/Src/Myc.DataRecord.pas
T
2025-07-19 14:24:04 +02:00

197 lines
6.1 KiB
ObjectPascal

unit Myc.DataRecord;
interface
uses
System.SysUtils,
System.Rtti,
System.TypInfo;
type
// A record that provides field-level access to data, stored in a packed byte buffer.
TDataRecord = record
public
type
IField = interface
procedure GetRaw(Dst: Pointer);
procedure SetRaw(Src: Pointer);
end;
IField<T> = interface(IField)
{$region 'private'}
function GetValue: T;
procedure SetValue(const Value: T);
{$endregion}
property Value: T read GetValue write SetValue;
end;
TFieldLayout = record
Name: string;
Offset: Integer;
Size: Integer;
TypeInfo: PTypeInfo;
end;
IBuilder = interface
procedure AddField(const Name: String; const Value: TValue); overload;
procedure SetupRecord(out Layout: TArray<TFieldLayout>; out Buffer: TBytes);
end;
// The interface helper record that provides the generic AddField<T> method.
TBuilder = record
private
FBuilder: IBuilder;
public
constructor Create(const ABuilder: IBuilder);
// This generic method is the reason for the helper's existence.
procedure AddField<T>(const Name: String; const Value: T); inline;
// Wrapper for methods on the underlying interface.
function CreateRec: TDataRecord; inline;
// Implicit operators for seamless casting between the helper and the interface.
class operator Implicit(const AValue: IBuilder): TBuilder; overload;
class operator Implicit(const AValue: TBuilder): IBuilder; overload;
end;
private
FLayout: TArray<TFieldLayout>;
FBuffer: TBytes;
public
class operator Initialize(out Dest: TDataRecord);
class operator Finalize(var Dest: TDataRecord);
class operator Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord);
// Factory method now returns the helper record instance.
class function CreateBuilder: TBuilder; static;
function IndexOf(const Name: String): Integer;
function GetField(const Name: String): IField; overload;
function GetField<T>(const Name: String): IField<T>; overload;
property Layout: TArray<TFieldLayout> read FLayout;
end;
implementation
uses
System.Classes,
System.Generics.Collections,
Myc.DataRecord.Impl;
{ TDataRecord.TBuilder }
constructor TDataRecord.TBuilder.Create(const ABuilder: IBuilder);
begin
FBuilder := ABuilder;
end;
procedure TDataRecord.TBuilder.AddField<T>(const Name: String; const Value: T);
begin
FBuilder.AddField(Name, TValue.From<T>(Value));
end;
function TDataRecord.TBuilder.CreateRec: TDataRecord;
begin
FBuilder.SetupRecord(Result.FLayout, Result.FBuffer);
end;
class operator TDataRecord.TBuilder.Implicit(const AValue: IBuilder): TBuilder;
begin
Result.FBuilder := AValue;
end;
class operator TDataRecord.TBuilder.Implicit(const AValue: TBuilder): IBuilder;
begin
Result := AValue.FBuilder;
end;
{ TDataRecord }
function TDataRecord.GetField<T>(const Name: String): IField<T>;
var
idx: Integer;
begin
idx := IndexOf(Name);
if idx < 0 then
exit;
if TypeInfo(T) <> FLayout[idx].TypeInfo then
exit;
Result := TGenericField<T>.Create(FBuffer, FLayout[idx]);
end;
class function TDataRecord.CreateBuilder: TBuilder;
begin
// Create the implementation class and wrap it in the helper record.
Result := TDataRecord.TBuilder.Create(TDataRecordBuilder.Create);
end;
function TDataRecord.GetField(const Name: String): IField;
var
idx: Integer;
begin
idx := IndexOf(Name);
if idx < 0 then
exit;
var typeInfo := FLayout[idx].TypeInfo;
case typeInfo.Kind of
tkInteger: Result := TIntegerField.Create(FBuffer, FLayout[idx]);
tkFloat:
case GetTypeData(typeInfo).FloatType of
ftSingle: Result := TSingleField.Create(FBuffer, FLayout[idx]);
ftDouble: Result := TDoubleField.Create(FBuffer, FLayout[idx]);
end;
tkInt64: Result := TInt64Field.Create(FBuffer, FLayout[idx]);
tkString: Result := TStringField.Create(FBuffer, FLayout[idx]);
end;
if not Assigned(Result) then
Result := TField.Create(FBuffer, FLayout[idx]);
end;
function TDataRecord.IndexOf(const Name: String): Integer;
var
dummyLayout: TFieldLayout;
begin
dummyLayout.Name := Name;
if not TArray.BinarySearch<TFieldLayout>(FLayout, dummyLayout, Result, TFieldLayoutComparer.DefaultComparer) then
Result := -1;
end;
class operator TDataRecord.Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord);
begin
Finalize(Dest);
Dest.FLayout := Src.FLayout;
SetLength(Dest.FBuffer, Length(Src.FBuffer));
for var layout in Dest.FLayout do
begin
var P: PByte := @Dest.FBuffer[layout.Offset];
var Q: PByte := @Src.FBuffer[layout.Offset];
var Val: TValue;
TValue.Make(Q, layout.TypeInfo, val);
val.ExtractRawData(P);
end;
end;
class operator TDataRecord.Initialize(out Dest: TDataRecord);
begin
Dest.FLayout := nil;
Dest.FBuffer := nil;
end;
class operator TDataRecord.Finalize(var Dest: TDataRecord);
begin
if (Dest.FBuffer = nil) or (Dest.FLayout = nil) then
Exit;
for var layout in Dest.FLayout do
begin
var P: PByte := @Dest.FBuffer[layout.Offset];
var Val: TValue;
// IsMoved has to be false, because we want the local TValue to own the field and destroy it when it looses scope!
TValue.MakeWithoutCopy(P, layout.TypeInfo, val, false);
end;
Dest.FLayout := nil;
Dest.FBuffer := nil;
end;
end.