unit Myc.DataRecord; interface uses System.Rtti, System.SysUtils, System.Generics.Collections, System.Generics.Defaults; type // Provides generic access to a value of type T. IAccess = interface ['{1D422E3A-25C8-459B-A480-2E246942CF56}'] function GetValue: T; procedure SetValue(const Value: T); property Value: T read GetValue write SetValue; end; // A record that provides field-level access to data, stored in a sorted array. TDataRecord = record public type // Builder for dynamically creating a TDataRecord. TBuilder = record private // Use a list for efficient additions. Will be sorted on creation. FStagedValues: TList>; class operator Initialize(out Dest: TBuilder); class operator Finalize(var Dest: TBuilder); public procedure AddField(const Name: String; const Value: T); function CreateRec: TDataRecord; end; private FValues: TArray>; class operator Initialize(out Dest: TDataRecord); class operator Finalize(var Dest: TDataRecord); public function GetAccess(const Name: String): IAccess; class function CreateFromRec(const Rec: T): TDataRecord; static; class function CreateBuilder: TDataRecord.TBuilder; static; class function CreateFromJSON(const JSON: string): TDataRecord; static; end; TValueAccessor = class(TInterfacedObject, IAccess) private FValues: TArray>; FKey: string; function FindIndex: Integer; public constructor Create(AValues: TArray>; const AKey: string); function GetValue: T; procedure SetValue(const Value: T); end; // Comparer for sorting and searching pairs by their string key (Singleton). TPairKeyComparer = class(TComparer>) strict private class var FDefault: IComparer>; class constructor CreateClass; class destructor DestroyClass; public function Compare(const Left, Right: TPair): Integer; override; class property Default: IComparer> read FDefault; end; procedure Testfunc; implementation uses System.TypInfo, System.JSON; {$region 'TPairKeyComparer'} class constructor TPairKeyComparer.CreateClass; begin FDefault := TPairKeyComparer.Create; end; class destructor TPairKeyComparer.DestroyClass; begin FDefault := nil; end; function TPairKeyComparer.Compare(const Left, Right: TPair): Integer; begin Result := TComparer.Default.Compare(Left.Key, Right.Key); end; {$endregion} {$region 'TValueAccessor'} constructor TValueAccessor.Create(AValues: TArray>; const AKey: string); begin inherited Create; FValues := AValues; FKey := AKey; end; function TValueAccessor.FindIndex: Integer; var dummyPair: TPair; begin // Find the index of the key using binary search with the singleton comparer. dummyPair := TPair.Create(FKey, TValue.Empty); if not TArray.BinarySearch>(FValues, dummyPair, Result, TPairKeyComparer.Default) then Result := -1; end; function TValueAccessor.GetValue: T; var idx: Integer; begin idx := FindIndex; if idx > -1 then Result := FValues[idx].Value.AsType else raise EArgumentException.CreateFmt('Key not found: %s', [FKey]); end; procedure TValueAccessor.SetValue(const Value: T); var idx: Integer; begin idx := FindIndex; if idx > -1 then // Directly modify the TValue within the TPair in the array. FValues[idx].Value := TValue.From(Value) else raise EArgumentException.CreateFmt('Key not found: %s', [FKey]); end; {$endregion} {$region 'TDataRecord'} function TDataRecord.GetAccess(const Name: String): IAccess; var dummyPair: TPair; val: TValue; index: Integer; targetTypeInfo: PTypeInfo; begin Result := nil; // Use a binary search on the sorted array with the singleton comparer. dummyPair := TPair.Create(Name, TValue.Empty); if TArray.BinarySearch>(FValues, dummyPair, index, TPairKeyComparer.Default) then begin val := FValues[index].Value; targetTypeInfo := System.TypeInfo(T); if (val.TypeInfo <> nil) and (val.TypeInfo = targetTypeInfo) then begin Result := TValueAccessor.Create(FValues, Name); end; end; end; class function TDataRecord.CreateFromRec(const Rec: T): TDataRecord; var ctx: TRttiContext; recType: TRttiRecordType; field: TRttiField; recValue: TValue; stagedValues: TList>; begin stagedValues := TList>.Create; try ctx := TRttiContext.Create; recType := ctx.GetType(TypeInfo(T)) as TRttiRecordType; recValue := TValue.From(Rec); for field in recType.GetFields do stagedValues.Add(TPair.Create(field.Name, field.GetValue(recValue.GetReferenceToRawData))); // Sort the list and assign it to the result array. stagedValues.Sort(TPairKeyComparer.Default); Result.FValues := stagedValues.ToArray; finally stagedValues.Free; end; end; class function TDataRecord.CreateFromJSON(const JSON: string): TDataRecord; var jsonValue: TJSONValue; jsonObject: TJSONObject; pair: TJSONPair; pairValue: TJSONValue; s: string; b: Boolean; i64: Int64; dbl: Double; stagedValues: TList>; begin stagedValues := TList>.Create; try jsonValue := TJSONObject.ParseJSONValue(JSON); if not Assigned(jsonValue) or not (jsonValue is TJSONObject) then begin Result.FValues := []; Exit; end; try jsonObject := jsonValue as TJSONObject; for pair in jsonObject do begin pairValue := pair.JsonValue; if pairValue is TJSONNumber then begin if pairValue.TryGetValue(i64) then stagedValues.Add(TPair.Create(pair.JsonString.Value, TValue.From(i64))) else if pairValue.TryGetValue(dbl) then stagedValues.Add(TPair.Create(pair.JsonString.Value, TValue.From(dbl))); end else if pairValue is TJSONString then begin if pairValue.TryGetValue(s) then stagedValues.Add(TPair.Create(pair.JsonString.Value, TValue.From(s))); end else if pairValue is TJSONBool then begin if pairValue.TryGetValue(b) then stagedValues.Add(TPair.Create(pair.JsonString.Value, TValue.From(b))); end else if pairValue is TJSONNull then stagedValues.Add(TPair.Create(pair.JsonString.Value, TValue.Empty)); end; finally jsonValue.Free; end; // Sort the list and assign it to the result array. stagedValues.Sort(TPairKeyComparer.Default); Result.FValues := stagedValues.ToArray; finally stagedValues.Free; end; end; class function TDataRecord.CreateBuilder: TDataRecord.TBuilder; begin Result := Default(TDataRecord.TBuilder); end; class operator TDataRecord.Initialize(out Dest: TDataRecord); begin // Initialize the array for a new TDataRecord instance. Dest.FValues := nil; end; class operator TDataRecord.Finalize(var Dest: TDataRecord); begin // Finalize the array. Dest.FValues := nil; end; {$endregion} {$region 'TDataRecord.TBuilder'} procedure TDataRecord.TBuilder.AddField(const Name: String; const Value: T); begin FStagedValues.Add(TPair.Create(Name, TValue.From(Value))); end; function TDataRecord.TBuilder.CreateRec: TDataRecord; begin // Sort the staged list and create the final record's array from it. FStagedValues.Sort(TPairKeyComparer.Default); Result.FValues := FStagedValues.ToArray; FStagedValues.Clear; // Clear the list after consuming it. end; class operator TDataRecord.TBuilder.Initialize(out Dest: TBuilder); begin Dest.FStagedValues := TList>.Create; end; class operator TDataRecord.TBuilder.Finalize(var Dest: TBuilder); begin Dest.FStagedValues.Free; end; {$endregion} type TTestRec = record A, B, C: Integer; Name: String; Val: Double; end; procedure Testfunc; var testRec: TTestRec; R: TDataRecord; nameAccess: IAccess; bAccess: IAccess; const JSON_DATA = '{"IntValue": 123, "StringValue": "Hello JSON", "FloatValue": 99.9, "BoolValue": true, "NullValue": null}'; begin testRec.A := 1; testRec.B := 5; testRec.C := 10; testRec.Name := 'Hi'; testRec.Val := 3.14; R := TDataRecord.CreateFromRec(testRec); // Test reading a string value nameAccess := R.GetAccess('Name'); Assert(Assigned(nameAccess)); Assert(nameAccess.Value = 'Hi'); // Test reading an integer value bAccess := R.GetAccess('B'); Assert(Assigned(bAccess)); Assert(bAccess.Value = 5); // Test writing a value bAccess.Value := 99; Assert(bAccess.Value = 99); // Verify that a request for a wrong type returns nil var aAccess := R.GetAccess('A'); Assert(not Assigned(aAccess)); // Test the builder pattern var RecBuilder: TDataRecord.TBuilder := TDataRecord.CreateBuilder; RecBuilder.AddField('ZValue', 222.0); // Changed key to test sorting RecBuilder.AddField('AValue', 'Builder Test'); var S := RecBuilder.CreateRec; var dblAccess := S.GetAccess('ZValue'); Assert(Assigned(dblAccess)); Assert(dblAccess.Value = 222.0); var strAccess := S.GetAccess('AValue'); Assert(Assigned(strAccess)); Assert(strAccess.Value = 'Builder Test'); // Test JSON parsing var jsonRec := TDataRecord.CreateFromJSON(JSON_DATA); var intAccess := jsonRec.GetAccess('IntValue'); Assert(Assigned(intAccess)); Assert(intAccess.Value = 123); strAccess := jsonRec.GetAccess('StringValue'); Assert(Assigned(strAccess)); Assert(strAccess.Value = 'Hello JSON'); dblAccess := jsonRec.GetAccess('FloatValue'); Assert(Assigned(dblAccess)); Assert(dblAccess.Value = 99.9); var boolAccess := jsonRec.GetAccess('BoolValue'); Assert(Assigned(boolAccess)); Assert(boolAccess.Value = true); end; end.