Types-JSON

This commit is contained in:
Michael Schimmel
2025-08-27 18:00:01 +02:00
parent dc4066097c
commit afb8f459c6
7 changed files with 830 additions and 133 deletions
+258
View File
@@ -0,0 +1,258 @@
unit Myc.Data.Types.JSON;
interface
uses
System.SysUtils,
System.JSON,
Myc.Data.Types;
type
// Provides methods for serializing and deserializing data values to/from JSON using the TDataType helpers.
TDataJsonConverter = class
public
// Serializes a data value to a TJSONValue.
class function ValueToJson(const Value: TDataType.TValue): TJSONValue;
// Deserializes a TJSONValue to a data value based on the provided data type.
class function JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue;
end;
implementation
uses
System.Rtti,
System.TypInfo,
System.DateUtils,
System.Math;
{ TDataJsonConverter }
class function TDataJsonConverter.ValueToJson(const Value: TDataType.TValue): TJSONValue;
var
jsonObject: TJSONObject;
jsonArray: TJSONArray;
i: Integer;
begin
if not Assigned(Value.DataValue) then
begin
Result := TJSONNull.Create;
Exit;
end;
case Value.DataType.Kind of
dkVoid: Result := TJSONNull.Create;
dkOrdinal: Result := TJSONNumber.Create(Value.AsOrdinal.Value);
dkFloat: Result := TJSONNumber.Create(Value.AsFloat.Value);
dkText: Result := TJSONString.Create(Value.AsText.Value);
dkTimestamp: Result := TJSONString.Create(DateToISO8601(Value.AsTimestamp.Value, true));
dkDecimal:
begin
// Serialize as string to preserve precision.
var decValue := Value.AsDecimal;
var val := decValue.Value;
var scale := decValue.DataType.Scale;
var s: string;
if (scale = 0) then
s := IntToStr(val)
else
begin
s := IntToStr(Abs(val));
if (Length(s) <= scale) then
s := StringOfChar('0', scale - Length(s) + 1) + s;
s := s.Insert(Length(s) - scale, '.');
if (val < 0) then
s := '-' + s;
end;
Result := TJSONString.Create(s);
end;
dkEnum:
begin
var enumValue := Value.AsEnum;
Result := TJSONString.Create(enumValue.DataType.Identifiers[enumValue.Value]);
end;
dkRecord:
begin
var recordValue := Value.AsRecord;
var recordType := recordValue.DataType;
jsonObject := TJSONObject.Create;
Result := jsonObject;
for i := 0 to recordType.FieldCount - 1 do
begin
jsonObject.AddPair(
recordType.Fields[i].Name,
ValueToJson(recordValue.Items[i]) // Items property returns TDataType.TValue
);
end;
end;
dkTuple:
begin
var tupleValue := Value.AsTuple;
jsonArray := TJSONArray.Create;
Result := jsonArray;
for i := 0 to tupleValue.ItemCount - 1 do
jsonArray.AddElement(ValueToJson(tupleValue.Items[i]));
end;
dkArray:
begin
var arrayValue := Value.AsArray;
jsonArray := TJSONArray.Create;
Result := jsonArray;
for i := 0 to arrayValue.ElementCount - 1 do
jsonArray.AddElement(ValueToJson(arrayValue.Items[i]));
end;
dkVector:
begin
var vectorValue := Value.AsVector;
jsonArray := TJSONArray.Create;
Result := jsonArray;
for i := 0 to vectorValue.ElementCount - 1 do
jsonArray.AddElement(ValueToJson(vectorValue.Items[i]));
end;
dkMethod: raise ENotSupportedException.Create('Cannot serialize a method type to JSON.');
else
raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(Value.DataType.Kind))]);
end;
end;
class function TDataJsonConverter.JsonToValue(const Json: TJSONValue; const DataType: TDataType): TDataType.TValue;
var
jsonObject: TJSONObject;
jsonArray: TJSONArray;
items: TArray<IDataValue>;
i, idx: Integer;
val: Int64;
begin
Assert(Assigned(DataType.DataType), 'DataType cannot be nil for deserialization');
case DataType.Kind of
dkVoid:
if (Json is TJSONNull) then
Result := TDataType.Void.Value
else
raise EConvertError.Create('JSON Null expected for Void type');
dkOrdinal: Result := DataType.AsOrdinal.CreateValue((Json as TJSONNumber).AsInt64);
dkFloat: Result := DataType.AsFloat.CreateValue((Json as TJSONNumber).AsDouble);
dkText: Result := DataType.AsText.CreateValue((Json as TJSONString).Value);
dkTimestamp: Result := DataType.AsTimestamp.CreateValue(ISO8601ToDate((Json as TJSONString).Value, True));
dkDecimal:
begin
// Handle string representation for precision, with fallback to number for compatibility.
var decimalType := DataType.AsDecimal;
if (Json is TJSONString) then
begin
var s := (Json as TJSONString).Value.Trim;
var isNegative := s.StartsWith('-');
if isNegative then
s := s.Remove(0, 1); // remove sign
var dotPos := Pos('.', s);
var numFracDigits: Integer;
if (dotPos = 0) then
numFracDigits := 0
else
begin
numFracDigits := Length(s) - dotPos;
s := s.Remove(dotPos - 1, 1); // remove dot
end;
val := StrToInt64(s);
var scaleDiff := decimalType.Scale - numFracDigits;
if (scaleDiff > 0) then // Not enough fractional digits
begin
for i := 1 to scaleDiff do
val := val * 10;
end
else if (scaleDiff < 0) then // Too many fractional digits
begin
for i := 1 to -scaleDiff do
val := val div 10;
end;
if isNegative then
val := -val;
Result := decimalType.CreateValue(val);
end
else if (Json is TJSONNumber) then
begin
// Support numbers for compatibility, but this can lead to precision loss.
val := Round((Json as TJSONNumber).AsDouble * Power(10, decimalType.Scale));
Result := decimalType.CreateValue(val);
end
else
raise EConvertError.Create('JSON String or Number expected for Decimal type');
end;
dkEnum:
begin
var enumType := DataType.AsEnum;
idx := enumType.IndexOf((Json as TJSONString).Value);
if (idx < 0) then
raise EConvertError.CreateFmt('Invalid enum identifier "%s" for type %s', [Json.Value, DataType.Name]);
Result := enumType.CreateValue(idx);
end;
dkRecord:
begin
var recordType := DataType.AsRecord;
jsonObject := Json as TJSONObject;
SetLength(items, recordType.FieldCount);
for i := 0 to recordType.FieldCount - 1 do
begin
var field := recordType.Fields[i];
var jsonPair := jsonObject.Get(field.Name);
if not Assigned(jsonPair) then
raise EConvertError.CreateFmt('Missing field "%s" for record type %s', [field.Name, DataType.Name]);
// Recursive call returns TDataType.TValue, which implicitly converts to IDataValue for the array
items[i] := JsonToValue(jsonPair.JsonValue, field.DataType);
end;
Result := recordType.CreateValue(items);
end;
dkTuple:
begin
var tupleType := DataType.AsTuple;
jsonArray := Json as TJSONArray;
if (jsonArray.Count <> IDataTupleType(tupleType).ItemCount) then
raise EConvertError
.CreateFmt('JSON array size (%d) does not match tuple size (%d)', [jsonArray.Count, tupleType.ItemCount]);
SetLength(items, IDataTupleType(tupleType).ItemCount);
for i := 0 to IDataTupleType(tupleType).ItemCount - 1 do
begin
var itemType := IDataTupleType(tupleType).ItemType[i];
items[i] := JsonToValue(jsonArray.Items[i], itemType);
end;
Result := tupleType.CreateValue(items);
end;
dkArray:
begin
var arrayType := DataType.AsArray;
jsonArray := Json as TJSONArray;
SetLength(items, jsonArray.Count);
for i := 0 to jsonArray.Count - 1 do
items[i] := JsonToValue(jsonArray.Items[i], arrayType.ElementType);
Result := arrayType.CreateValue(items);
end;
dkVector:
begin
var vectorType := DataType.AsVector;
jsonArray := Json as TJSONArray;
if (jsonArray.Count <> vectorType.ElementCount) then
raise EConvertError
.CreateFmt('JSON array size (%d) does not match vector size (%d)', [jsonArray.Count, vectorType.ElementCount]);
SetLength(items, jsonArray.Count);
for i := 0 to jsonArray.Count - 1 do
items[i] := JsonToValue(jsonArray.Items[i], vectorType.ElementType);
Result := vectorType.CreateValue(items);
end;
dkMethod: raise ENotSupportedException.Create('Cannot deserialize a method type from JSON.');
else
raise ENotSupportedException.CreateFmt('Unsupported data kind: %s', [GetEnumName(TypeInfo(TDataKind), Ord(DataType.Kind))]);
end;
end;
end.
+9 -12
View File
@@ -7,10 +7,7 @@ uses
System.RTTI,
Myc.Data.Types;
type
TValueHelper = record helper for TDataType.TValue
function AsTValue: TValue;
end;
function AsTValue(const Value: TDataType.TValue): TValue;
function RttiTypeToDataType(rttiType: TRttiType): TDataType;
@@ -69,16 +66,16 @@ end;
{ TValueHelper }
function TValueHelper.AsTValue: TValue;
function AsTValue(const Value: TDataType.TValue): TValue;
begin
case DataType.Kind of
case Value.DataType.Kind of
dkVoid: Result := TValue.Empty;
dkOrdinal: Result := TValue.From<Int64>(AsOrdinal.Value);
dkFloat: Result := TValue.From<Double>(AsFloat.Value);
dkText: Result := TValue.From<string>(AsText.Value);
dkTimestamp: Result := TValue.From<TDateTime>(AsTimestamp.Value);
dkEnum: Result := TValue.From<Integer>(AsEnum.Value);
dkMethod: Result := TValue.From<TDataMethodProc>(AsMethod.Value);
dkOrdinal: Result := TValue.From<Int64>(Value.AsOrdinal.Value);
dkFloat: Result := TValue.From<Double>(Value.AsFloat.Value);
dkText: Result := TValue.From<string>(Value.AsText.Value);
dkTimestamp: Result := TValue.From<TDateTime>(Value.AsTimestamp.Value);
dkEnum: Result := TValue.From<Integer>(Value.AsEnum.Value);
dkMethod: Result := TValue.From<TDataMethodProc>(Value.AsMethod.Value);
else
raise EInvalidOpException.Create('Unsupported data type');
end;
+123 -98
View File
@@ -169,7 +169,13 @@ type
end;
IDataTupleType = interface(IDataType)
{$region 'private'}
function GetItemCount: Integer;
function GetItemType(Idx: Integer): IDataType;
{$endregion}
function CreateValue(const AItems: array of IDataValue): IDataTupleValue;
property ItemCount: Integer read GetItemCount;
property ItemType[Idx: Integer]: IDataType read GetItemType;
end;
IDataArrayValue = interface(IDataValue)
@@ -219,20 +225,6 @@ type
public
constructor Create(const ADataValue: IDataValue);
// Casting
function AsOrdinal: IDataOrdinalValue; inline;
function AsFloat: IDataFloatValue; inline;
function AsText: IDataTextValue; inline;
function AsTimestamp: IDataTimestampValue; inline;
function AsDecimal: IDataDecimalValue; inline;
function AsRecord: IDataRecordValue; inline;
function AsTuple: IDataTupleValue; inline;
function AsArray: IDataArrayValue; inline;
function AsVector: IDataVectorValue; inline;
function AsEnum: IDataEnumValue; inline;
function AsMethod: IDataMethodValue; inline;
function AsVoid: IDataVoidValue; inline;
class operator Implicit(const A: IDataValue): TDataType.TValue; overload; inline;
class operator Implicit(const A: TDataType.TValue): IDataValue; overload; inline;
@@ -466,11 +458,15 @@ type
end;
private
FTupleType: IDataTupleType;
function GetItemCount: Integer; inline;
function GetItemType(Idx: Integer): IDataType; inline;
public
constructor Create(const ATupleType: IDataTupleType);
class operator Implicit(const A: IDataTupleType): TDataType.TTuple; overload; inline;
class operator Implicit(const A: TDataType.TTuple): IDataTupleType; overload; inline;
function CreateValue(const AItems: array of IDataValue): TDataType.TTuple.TValue;
property ItemCount: Integer read GetItemCount;
property ItemType[Idx: Integer]: IDataType read GetItemType;
end;
TArray = record
@@ -560,6 +556,23 @@ type
function CreateValue(const Proc: TProc): TDataType.TMethod.TValue; inline;
end;
// Helper to cast a generic TValue to a specific value helper.
TValueHelper = record helper for TValue
public
function AsVoid: TVoid.TValue; inline;
function AsOrdinal: TOrdinal.TValue; inline;
function AsFloat: TFloat.TValue; inline;
function AsText: TText.TValue; inline;
function AsTimestamp: TTimestamp.TValue; inline;
function AsDecimal: TDecimal.TValue; inline;
function AsRecord: TRecord.TValue; inline;
function AsTuple: TTuple.TValue; inline;
function AsArray: TArray.TValue; inline;
function AsVector: TVector.TValue; inline;
function AsEnum: TEnum.TValue; inline;
function AsMethod: TMethod.TValue; inline;
end;
strict private
FDataType: IDataType;
function GetName: String; inline;
@@ -1247,6 +1260,16 @@ begin
Result.Create(FTupleType.CreateValue(AItems));
end;
function TDataType.TTuple.GetItemCount: Integer;
begin
Result := FTupleType.ItemCount;
end;
function TDataType.TTuple.GetItemType(Idx: Integer): IDataType;
begin
Result := FTupleType.ItemType[Idx];
end;
class operator TDataType.TTuple.Implicit(const A: IDataTupleType): TDataType.TTuple;
begin
Result.FTupleType := A;
@@ -1590,90 +1613,6 @@ begin
FDataValue := ADataValue;
end;
function TDataType.TValue.AsArray: IDataArrayValue;
begin
if (FDataValue.DataType.Kind <> dkArray) then
raise EInvalidCast.Create('Array expected');
Result := IDataArrayValue(FDataValue);
end;
function TDataType.TValue.AsDecimal: IDataDecimalValue;
begin
if (FDataValue.DataType.Kind <> dkDecimal) then
raise EInvalidCast.Create('Decimal expected');
Result := IDataDecimalValue(FDataValue);
end;
function TDataType.TValue.AsEnum: IDataEnumValue;
begin
if (FDataValue.DataType.Kind <> dkEnum) then
raise EInvalidCast.Create('Enum expected');
Result := IDataEnumValue(FDataValue);
end;
function TDataType.TValue.AsFloat: IDataFloatValue;
begin
if (FDataValue.DataType.Kind <> dkFloat) then
raise EInvalidCast.Create('Float expected');
Result := IDataFloatValue(FDataValue);
end;
function TDataType.TValue.AsMethod: IDataMethodValue;
begin
if (FDataValue.DataType.Kind <> dkMethod) then
raise EInvalidCast.Create('Method expected');
Result := IDataMethodValue(FDataValue);
end;
function TDataType.TValue.AsOrdinal: IDataOrdinalValue;
begin
if (FDataValue.DataType.Kind <> dkOrdinal) then
raise EInvalidCast.Create('Ordinal expected');
Result := IDataOrdinalValue(FDataValue);
end;
function TDataType.TValue.AsRecord: IDataRecordValue;
begin
if (FDataValue.DataType.Kind <> dkRecord) then
raise EInvalidCast.Create('Record expected');
Result := IDataRecordValue(FDataValue);
end;
function TDataType.TValue.AsText: IDataTextValue;
begin
if (FDataValue.DataType.Kind <> dkText) then
raise EInvalidCast.Create('Text expected');
Result := IDataTextValue(FDataValue);
end;
function TDataType.TValue.AsTimestamp: IDataTimestampValue;
begin
if (FDataValue.DataType.Kind <> dkTimestamp) then
raise EInvalidCast.Create('Timestamp expected');
Result := IDataTimestampValue(FDataValue);
end;
function TDataType.TValue.AsTuple: IDataTupleValue;
begin
if (FDataValue.DataType.Kind <> dkTuple) then
raise EInvalidCast.Create('Tuple expected');
Result := IDataTupleValue(FDataValue);
end;
function TDataType.TValue.AsVector: IDataVectorValue;
begin
if (FDataValue.DataType.Kind <> dkVector) then
raise EInvalidCast.Create('Vector expected');
Result := IDataVectorValue(FDataValue);
end;
function TDataType.TValue.AsVoid: IDataVoidValue;
begin
if (FDataValue.DataType.Kind <> dkVoid) then
raise EInvalidCast.Create('Void expected');
Result := IDataVoidValue(FDataValue);
end;
function TDataType.TValue.GetDataType: IDataType;
begin
if Assigned(FDataValue) then
@@ -1769,4 +1708,90 @@ begin
Result.Create(IDataVectorType(FDataType));
end;
{ TDataType.TValueHelper }
function TDataType.TValueHelper.AsArray: TDataType.TArray.TValue;
begin
if (FDataValue.DataType.Kind <> dkArray) then
raise EInvalidCast.Create('Array expected');
Result.Create(IDataArrayValue(FDataValue));
end;
function TDataType.TValueHelper.AsDecimal: TDecimal.TValue;
begin
if (FDataValue.DataType.Kind <> dkDecimal) then
raise EInvalidCast.Create('Decimal expected');
Result.Create(IDataDecimalValue(FDataValue));
end;
function TDataType.TValueHelper.AsEnum: TDataType.TEnum.TValue;
begin
if (FDataValue.DataType.Kind <> dkEnum) then
raise EInvalidCast.Create('Enum expected');
Result.Create(IDataEnumValue(FDataValue));
end;
function TDataType.TValueHelper.AsFloat: TDataType.TFloat.TValue;
begin
if (FDataValue.DataType.Kind <> dkFloat) then
raise EInvalidCast.Create('Float expected');
Result.Create(IDataFloatValue(FDataValue));
end;
function TDataType.TValueHelper.AsMethod: TDataType.TMethod.TValue;
begin
if (FDataValue.DataType.Kind <> dkMethod) then
raise EInvalidCast.Create('Method expected');
Result.Create(IDataMethodValue(FDataValue));
end;
function TDataType.TValueHelper.AsOrdinal: TDataType.TOrdinal.TValue;
begin
if (FDataValue.DataType.Kind <> dkOrdinal) then
raise EInvalidCast.Create('Ordinal expected');
Result.Create(IDataOrdinalValue(FDataValue));
end;
function TDataType.TValueHelper.AsRecord: TDataType.TRecord.TValue;
begin
if (FDataValue.DataType.Kind <> dkRecord) then
raise EInvalidCast.Create('Record expected');
Result.Create(IDataRecordValue(FDataValue));
end;
function TDataType.TValueHelper.AsText: TDataType.TText.TValue;
begin
if (FDataValue.DataType.Kind <> dkText) then
raise EInvalidCast.Create('Text expected');
Result.Create(IDataTextValue(FDataValue));
end;
function TDataType.TValueHelper.AsTimestamp: TDataType.TTimestamp.TValue;
begin
if (FDataValue.DataType.Kind <> dkTimestamp) then
raise EInvalidCast.Create('Timestamp expected');
Result.Create(IDataTimestampValue(FDataValue));
end;
function TDataType.TValueHelper.AsTuple: TDataType.TTuple.TValue;
begin
if (FDataValue.DataType.Kind <> dkTuple) then
raise EInvalidCast.Create('Tuple expected');
Result.Create(IDataTupleValue(FDataValue));
end;
function TDataType.TValueHelper.AsVector: TDataType.TVector.TValue;
begin
if (FDataValue.DataType.Kind <> dkVector) then
raise EInvalidCast.Create('Vector expected');
Result.Create(IDataVectorValue(FDataValue));
end;
function TDataType.TValueHelper.AsVoid: TDataType.TVoid.TValue;
begin
if (FDataValue.DataType.Kind <> dkVoid) then
raise EInvalidCast.Create('Void expected');
Result.Create(IDataVoidValue(FDataValue));
end;
end.
+412
View File
@@ -0,0 +1,412 @@
unit TestDataTypes.JSON;
interface
uses
System.JSON,
DUnitX.TestFramework;
type
[TestFixture]
[IgnoreMemoryLeaks]
TTestDataTypesJSON = class
private
FJson: TJSONValue;
public
[Setup]
procedure SetUp;
[TearDown]
procedure TearDown;
[Test]
procedure TestVoid;
[Test]
[TestCase('Positive', '12345')]
[TestCase('Negative', '-54321')]
[TestCase('Zero', '0')]
procedure TestOrdinal(const AValue: Int64);
[Test]
[TestCase('Positive', '123.456')]
[TestCase('Negative', '-543.21')]
[TestCase('Zero', '0.0')]
procedure TestFloat(const AValue: Double);
[Test]
[TestCase('Simple', 'Hello World')]
[TestCase('Empty', '')]
[TestCase('SpecialChars', 'äöüß#+*?%&/()=')]
procedure TestText(const AValue: string);
[Test]
procedure TestTimestamp;
[Test]
[TestCase('Scale4_Positive', '1234567,4,123.4567')]
[TestCase('Scale2_Negative', '-987,2,-9.87')]
[TestCase('Scale5_Small', '123,5,0.00123')]
[TestCase('Scale0_Int', '123,0,123')]
[TestCase('Scale2_Zero', '0,2,0.00')]
[TestCase('Scale0_Zero', '0,0,0')]
procedure TestDecimal(const AValue: Int64; AScale: Integer; const AExpectedJsonString: string);
[Test]
procedure TestEnum;
[Test]
procedure TestRecord;
[Test]
procedure TestTuple;
[Test]
procedure TestArray;
[Test]
procedure TestVector;
[Test]
procedure TestMethodFails;
[Test]
procedure TestNullValue;
end;
implementation
uses
System.SysUtils,
System.DateUtils,
System.Math,
Myc.Data.Types,
Myc.Data.Types.JSON;
{ TTestDataTypesJSON }
procedure TTestDataTypesJSON.SetUp;
begin
FJson := nil;
end;
procedure TTestDataTypesJSON.TearDown;
begin
FJson.Free;
end;
procedure TTestDataTypesJSON.TestVoid;
var
voidType: TDataType.TVoid;
voidValue: TDataType.TVoid.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
voidType := TDataType.Void;
voidValue := voidType.Value;
// Serialize
FJson := TDataJsonConverter.ValueToJson(voidValue);
Assert.IsNotNull(FJson, 'JSON value should not be nil');
Assert.IsTrue(FJson is TJSONNull, 'Void should serialize to JSON Null');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, voidType);
Assert.IsNotNull(deserializedValue.DataValue, 'Deserialized value should not be nil');
Assert.AreEqual(dkVoid, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkVoid');
end;
procedure TTestDataTypesJSON.TestOrdinal(const AValue: Int64);
var
ordType: TDataType.TOrdinal;
ordValue: TDataType.TOrdinal.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
ordType := TDataType.Ordinal;
ordValue := ordType.CreateValue(AValue);
// Serialize
FJson := TDataJsonConverter.ValueToJson(ordValue);
Assert.IsTrue(FJson is TJSONNumber, 'Ordinal should serialize to JSON Number');
Assert.AreEqual(AValue, (FJson as TJSONNumber).AsInt64, 'Serialized value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, ordType);
Assert.AreEqual(dkOrdinal, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkOrdinal');
Assert.AreEqual(AValue, deserializedValue.AsOrdinal.Value, 'Deserialized value mismatch');
end;
procedure TTestDataTypesJSON.TestFloat(const AValue: Double);
var
floatType: TDataType.TFloat;
floatValue: TDataType.TFloat.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
floatType := TDataType.Float;
floatValue := floatType.CreateValue(AValue);
// Serialize
FJson := TDataJsonConverter.ValueToJson(floatValue);
Assert.IsTrue(FJson is TJSONNumber, 'Float should serialize to JSON Number');
Assert.AreEqual(AValue, (FJson as TJSONNumber).AsDouble, 1E-9, 'Serialized value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, floatType);
Assert.AreEqual(dkFloat, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkFloat');
Assert.AreEqual(AValue, deserializedValue.AsFloat.Value, 1E-9, 'Deserialized value mismatch');
end;
procedure TTestDataTypesJSON.TestText(const AValue: string);
var
textType: TDataType.TText;
textValue: TDataType.TText.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
textType := TDataType.Text;
textValue := textType.CreateValue(AValue);
// Serialize
FJson := TDataJsonConverter.ValueToJson(textValue);
Assert.IsTrue(FJson is TJSONString, 'Text should serialize to JSON String');
Assert.AreEqual(AValue, (FJson as TJSONString).Value, 'Serialized value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, textType);
Assert.AreEqual(dkText, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkText');
Assert.AreEqual(AValue, deserializedValue.AsText.Value, 'Deserialized value mismatch');
end;
procedure TTestDataTypesJSON.TestTimestamp;
var
tsType: TDataType.TTimestamp;
tsValue: TDataType.TTimestamp.TValue;
deserializedValue: TDataType.TValue;
testVal: TDateTime;
begin
// Setup
testVal := EncodeDateTime(2025, 8, 27, 17, 30, 15, 500);
tsType := TDataType.Timestamp;
tsValue := tsType.CreateValue(testVal);
// Serialize
FJson := TDataJsonConverter.ValueToJson(tsValue);
Assert.IsTrue(FJson is TJSONString, 'Timestamp should serialize to JSON String');
Assert.AreEqual(DateToISO8601(testVal, true), (FJson as TJSONString).Value, 'Serialized value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, tsType);
Assert.AreEqual(dkTimestamp, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkTimestamp');
Assert.IsTrue(CompareDateTime(testVal, deserializedValue.AsTimestamp.Value) = 0, 'Deserialized value mismatch');
end;
procedure TTestDataTypesJSON.TestDecimal(const AValue: Int64; AScale: Integer; const AExpectedJsonString: string);
var
decType: TDataType.TDecimal;
decValue: TDataType.TDecimal.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
decType := TDataType.DecimalOf(AScale);
decValue := decType.CreateValue(AValue);
// Serialize
FJson := TDataJsonConverter.ValueToJson(decValue);
Assert.IsTrue(FJson is TJSONString, 'Decimal should serialize to JSON String');
Assert.AreEqual(AExpectedJsonString, (FJson as TJSONString).Value, 'Serialized string value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, decType);
Assert.AreEqual(dkDecimal, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkDecimal');
Assert.AreEqual(AValue, deserializedValue.AsDecimal.Value, 'Deserialized value mismatch');
Assert.AreEqual(AScale, deserializedValue.AsDecimal.DataType.Scale, 'Deserialized scale mismatch');
// Test Deserialization from Number (compatibility)
FJson.Free; // Free previous value before creating new one
FJson := TJSONNumber.Create(AValue / Power(10, AScale));
deserializedValue := TDataJsonConverter.JsonToValue(FJson, decType);
Assert.AreEqual(AValue, deserializedValue.AsDecimal.Value, 'Deserialized value from number mismatch');
end;
procedure TTestDataTypesJSON.TestEnum;
var
enumType: TDataType.TEnum;
enumValue: TDataType.TEnum.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
enumType := TDataType.EnumOf('MyEnum', ['Red', 'Green', 'Blue']);
enumValue := enumType.CreateValue(1); // Green
// Serialize
FJson := TDataJsonConverter.ValueToJson(enumValue);
Assert.IsTrue(FJson is TJSONString, 'Enum should serialize to JSON String');
Assert.AreEqual('Green', (FJson as TJSONString).Value, 'Serialized value mismatch');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, enumType);
Assert.AreEqual(dkEnum, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkEnum');
Assert.AreEqual(1, deserializedValue.AsEnum.Value, 'Deserialized value mismatch');
end;
procedure TTestDataTypesJSON.TestRecord;
var
recordType: TDataType.TRecord;
idVal: TDataType.TOrdinal.TValue;
nameVal: TDataType.TText.TValue;
recordValue: TDataType.TRecord.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
recordType := TDataType.RecordOf([TDataRecordField.Create('ID', TDataType.Ordinal), TDataRecordField.Create('Name', TDataType.Text)]);
idVal := TDataType.Ordinal.CreateValue(99);
nameVal := TDataType.Text.CreateValue('TestRecord');
recordValue := recordType.CreateValue([idVal, nameVal]);
// Serialize
FJson := TDataJsonConverter.ValueToJson(recordValue);
Assert.IsTrue(FJson is TJSONObject, 'Record should serialize to JSON Object');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, recordType);
Assert.AreEqual(dkRecord, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkRecord');
var desRecord := deserializedValue.AsRecord;
Assert.AreEqual(Int64(99), desRecord.Items[0].AsOrdinal.Value, 'Deserialized record field ID mismatch');
Assert.AreEqual('TestRecord', desRecord.Items[1].AsText.Value, 'Deserialized record field Name mismatch');
end;
procedure TTestDataTypesJSON.TestTuple;
var
tupleType: TDataType.TTuple;
idVal: TDataType.TOrdinal.TValue;
nameVal: TDataType.TText.TValue;
tupleValue: TDataType.TTuple.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
tupleType := TDataType.TupleOf([TDataType.Ordinal, TDataType.Text]);
idVal := TDataType.Ordinal.CreateValue(101);
nameVal := TDataType.Text.CreateValue('TestTuple');
tupleValue := tupleType.CreateValue([idVal, nameVal]);
// Serialize
FJson := TDataJsonConverter.ValueToJson(tupleValue);
Assert.IsTrue(FJson is TJSONArray, 'Tuple should serialize to JSON Array');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, tupleType);
Assert.AreEqual(dkTuple, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkTuple');
var desTuple := deserializedValue.AsTuple;
Assert.AreEqual(Int64(101), desTuple.Items[0].AsOrdinal.Value, 'Deserialized tuple item 0 mismatch');
Assert.AreEqual('TestTuple', desTuple.Items[1].AsText.Value, 'Deserialized tuple item 1 mismatch');
end;
procedure TTestDataTypesJSON.TestArray;
var
arrayType: TDataType.TArray;
val1, val2: TDataType.TText.TValue;
arrayValue: TDataType.TArray.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
arrayType := TDataType.ArrayOf(TDataType.Text);
val1 := TDataType.Text.CreateValue('A');
val2 := TDataType.Text.CreateValue('B');
arrayValue := arrayType.CreateValue([val1, val2]);
// Serialize
FJson := TDataJsonConverter.ValueToJson(arrayValue);
Assert.IsTrue(FJson is TJSONArray, 'Array should serialize to JSON Array');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, arrayType);
Assert.AreEqual(dkArray, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkArray');
var desArray := deserializedValue.AsArray;
Assert.AreEqual(2, desArray.ElementCount, 'Deserialized array element count mismatch');
Assert.AreEqual('A', desArray.Items[0].AsText.Value, 'Deserialized array item 0 mismatch');
Assert.AreEqual('B', desArray.Items[1].AsText.Value, 'Deserialized array item 1 mismatch');
end;
procedure TTestDataTypesJSON.TestVector;
var
vectorType: TDataType.TVector;
val1, val2: TDataType.TOrdinal.TValue;
vectorValue: TDataType.TVector.TValue;
deserializedValue: TDataType.TValue;
jsonArray: TJSONArray;
begin
// Setup
vectorType := TDataType.VectorOf(TDataType.Ordinal, 2);
val1 := TDataType.Ordinal.CreateValue(10);
val2 := TDataType.Ordinal.CreateValue(20);
vectorValue := vectorType.CreateValue([val1, val2]);
// Serialize
FJson := TDataJsonConverter.ValueToJson(vectorValue);
Assert.IsTrue(FJson is TJSONArray, 'Vector should serialize to JSON Array');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, vectorType);
Assert.AreEqual(dkVector, deserializedValue.DataType.Kind, 'Deserialized type kind should be dkVector');
var desVector := deserializedValue.AsVector;
Assert.AreEqual(2, desVector.ElementCount, 'Deserialized vector element count mismatch');
Assert.AreEqual(Int64(10), desVector.Items[0].AsOrdinal.Value, 'Deserialized vector item 0 mismatch');
Assert.AreEqual(Int64(20), desVector.Items[1].AsOrdinal.Value, 'Deserialized vector item 1 mismatch');
// Test wrong size deserialization
jsonArray := nil;
try
jsonArray := TJSONArray.Create;
jsonArray.Add(1);
Assert.WillRaise(
procedure begin TDataJsonConverter.JsonToValue(jsonArray, vectorType); end,
EConvertError,
'Deserializing a JSON array with wrong size for a vector should raise an exception'
);
finally
jsonArray.Free;
end;
end;
procedure TTestDataTypesJSON.TestMethodFails;
var
methodType: TDataType.TMethod;
methodValue: TDataType.TMethod.TValue;
begin
// Setup
methodType := TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal);
methodValue := methodType.CreateValue(function(const AValue: TDataType.TValue): TDataType.TValue begin Result := AValue; end);
// Test Serialization
Assert.WillRaise(
procedure begin TDataJsonConverter.ValueToJson(methodValue).Free; end,
ENotSupportedException,
'Serializing a method should raise an exception'
);
// Test Deserialization
FJson := TJSONNull.Create;
//TODO Fails with "Method did not throw any exceptions. Deserializing a method should raise an exception"
Assert.WillRaise(
procedure begin TDataJsonConverter.JsonToValue(FJson, methodType); end,
ENotSupportedException,
'Deserializing a method should raise an exception'
);
end;
procedure TTestDataTypesJSON.TestNullValue;
var
value: TDataType.TValue;
deserializedValue: TDataType.TValue;
begin
// Setup
value := TDataType.Void.Value;
// Serialize
FJson := TDataJsonConverter.ValueToJson(value);
Assert.IsTrue(FJson is TJSONNull, 'nil IDataValue should serialize to JSON Null');
// Deserialize
deserializedValue := TDataJsonConverter.JsonToValue(FJson, TDataType.Void);
Assert.IsTrue(deserializedValue.DataType.Kind = dkVoid, 'JSON Null should deserialize to a TValue with a nil DataValue');
end;
initialization
TDUnitX.RegisterTestFixture(TTestDataTypesJSON);
end.
+23 -22
View File
@@ -8,7 +8,7 @@ uses
type
[TestFixture]
[IgnoreMemoryLeaks]
TMyTestObject = class
TTestDataTypes = class
public
[Test]
procedure TestRecords;
@@ -48,9 +48,10 @@ uses
System.Rtti,
System.TypInfo,
Myc.Data.Types,
Myc.Data.Types.RTTI;
Myc.Data.Types.RTTI,
Myc.Data.Types.JSON;
procedure TMyTestObject.TestRecords;
procedure TTestDataTypes.TestRecords;
var
intType: TDataType.TOrdinal;
floatType: TDataType.TFloat;
@@ -129,7 +130,7 @@ begin
);
end;
procedure TMyTestObject.TestRecords_Generic;
procedure TTestDataTypes.TestRecords_Generic;
var
dataType: TDataType.TRecord;
dataValue: TDataType.TRecord.TValue;
@@ -159,7 +160,7 @@ begin
Assert.AreEqual('<A: 1, B: two, C: 3>', IDataValue(dataValue).AsString, 'Generic record AsString is incorrect');
end;
procedure TMyTestObject.TestTuples;
procedure TTestDataTypes.TestTuples;
var
intValue: TDataType.TOrdinal.TValue;
floatValue: TDataType.TFloat.TValue;
@@ -193,7 +194,7 @@ begin
Assert.AreEqual('Tuple<Integer, Float>', type1.Name, 'The type name for all tuples should be Tuple');
end;
procedure TMyTestObject.TestTuples_Generic;
procedure TTestDataTypes.TestTuples_Generic;
var
tupleValue: TDataType.TTuple.TValue;
item: TDataType.TOrdinal.TValue;
@@ -219,7 +220,7 @@ begin
Assert.AreEqual('(1, 2, 3, 4, 5, 6)', IDataValue(tupleValue).AsString, 'Generic tuple AsString is incorrect');
end;
procedure TMyTestObject.TestArrays;
procedure TTestDataTypes.TestArrays;
var
intType: TDataType.TOrdinal;
floatType: TDataType.TFloat;
@@ -283,7 +284,7 @@ begin
);
end;
procedure TMyTestObject.TestArrays_OptimizedAndGeneric;
procedure TTestDataTypes.TestArrays_OptimizedAndGeneric;
var
arrayType: TDataType.TArray;
empty1, empty2, single, generic: TDataType.TArray.TValue;
@@ -316,7 +317,7 @@ begin
Assert.AreEqual('[1, 2, 3]', IDataValue(generic).AsString, 'Generic array AsString is incorrect');
end;
procedure TMyTestObject.TestTexts;
procedure TTestDataTypes.TestTexts;
var
textType: TDataType.TText;
textValue1, textValue2, castedValue: TDataType.TText.TValue;
@@ -354,7 +355,7 @@ begin
);
end;
procedure TMyTestObject.TestTexts_OptimizedEmpty;
procedure TTestDataTypes.TestTexts_OptimizedEmpty;
var
empty1, empty2: TDataType.TText.TValue;
begin
@@ -368,7 +369,7 @@ begin
Assert.AreEqual('', IDataValue(empty1).AsString, 'AsString of empty text should be empty');
end;
procedure TMyTestObject.TestFloats_OptimizedAndGeneric;
procedure TTestDataTypes.TestFloats_OptimizedAndGeneric;
var
zero1, zero2, nan1, nan2, generic: TDataType.TFloat.TValue;
begin
@@ -395,7 +396,7 @@ begin
Assert.AreEqual(123.45, generic.Value, 'Value of generic float is incorrect');
end;
procedure TMyTestObject.TestTimestamps;
procedure TTestDataTypes.TestTimestamps;
var
tsType: TDataType.TTimestamp;
tsValue1, tsValue2, castedValue: TDataType.TTimestamp.TValue;
@@ -435,7 +436,7 @@ begin
);
end;
procedure TMyTestObject.TestEnums;
procedure TTestDataTypes.TestEnums;
var
colorType: TDataType.TEnum;
red, green, blue: TDataType.TEnum.TValue;
@@ -490,7 +491,7 @@ begin
);
end;
procedure TMyTestObject.TestAsString;
procedure TTestDataTypes.TestAsString;
var
ordinalValue: TDataType.TOrdinal.TValue;
floatValue: TDataType.TFloat.TValue;
@@ -542,7 +543,7 @@ begin
Assert.AreEqual('(10, Tuple)', IDataValue(tupleValue).AsString, 'Tuple AsString incorrect');
end;
procedure TMyTestObject.TestAsTValue;
procedure TTestDataTypes.TestAsTValue;
var
dataValue: TDataType.TValue; // Keep as generic interface to test AsTValue
now: TDateTime;
@@ -551,21 +552,21 @@ begin
// This test is specifically for the IDataValue.AsTValue method. No changes needed.
// --- Ordinal ---
dataValue := TDataType.Ordinal.CreateValue(123);
var tv := dataValue.AsTValue;
var tv := AsTValue(dataValue);
Assert.IsFalse(tv.IsEmpty, 'Ordinal TValue should not be empty');
Assert.AreEqual(tkInt64, tv.Kind, 'Ordinal TValue kind should be tkInt64');
Assert.AreEqual(Int64(123), tv.AsInt64, 'Ordinal TValue content is incorrect');
// --- Float ---
dataValue := TDataType.Float.CreateValue(45.67);
tv := dataValue.AsTValue;
tv := AsTValue(dataValue);
Assert.IsFalse(tv.IsEmpty, 'Float TValue should not be empty');
Assert.AreEqual(tkFloat, tv.Kind, 'Float TValue kind should be tkFloat');
Assert.AreEqual(45.67, tv.AsExtended, 'Float TValue content is incorrect');
// --- Text ---
dataValue := TDataType.Text.CreateValue('Hello');
tv := dataValue.AsTValue;
tv := AsTValue(dataValue);
Assert.IsFalse(tv.IsEmpty, 'Text TValue should not be empty');
Assert.AreEqual(tkUString, tv.Kind, 'Text TValue kind should be tkUString');
Assert.AreEqual('Hello', tv.AsString, 'Text TValue content is incorrect');
@@ -573,7 +574,7 @@ begin
// --- Timestamp ---
now := System.SysUtils.Now;
dataValue := TDataType.Timestamp.CreateValue(now);
tv := dataValue.AsTValue;
tv := AsTValue(dataValue);
Assert.IsFalse(tv.IsEmpty, 'Timestamp TValue should not be empty');
Assert.AreEqual(tkFloat, tv.Kind, 'Timestamp TValue kind is incorrect');
Assert.AreEqual(now, tv.AsType<TDateTime>, 'Timestamp TValue content is incorrect');
@@ -581,13 +582,13 @@ begin
// --- Enum ---
colorType := TDataType.EnumOf('Color', ['Red', 'Green', 'Blue']);
dataValue := colorType.CreateValue('Green');
tv := dataValue.AsTValue;
tv := AsTValue(dataValue);
Assert.IsFalse(tv.IsEmpty, 'Enum TValue should not be empty');
Assert.AreEqual(tkInteger, tv.Kind, 'Enum TValue kind should be tkInteger');
Assert.AreEqual(1, tv.AsInteger, 'Enum TValue content is incorrect');
end;
procedure TMyTestObject.TestMethods;
procedure TTestDataTypes.TestMethods;
var
ordinalType: TDataType.TOrdinal;
textType: TDataType.TText;
@@ -658,7 +659,7 @@ begin
// --- 5. Test AsString and AsTValue ---
Assert.AreEqual('<METHOD>', IDataValue(methodValue).AsString, 'Method AsString is incorrect');
tv := TDataType.TValue(methodValue).AsTValue;
tv := AsTValue(methodValue);
Assert.IsFalse(tv.IsEmpty, 'Method TValue should not be empty');
Assert.AreEqual(tkInterface, tv.Kind, 'TValue kind for a TMethodProc should be tkInterface, as it is a reference-to-function');
Assert.IsTrue(TypeInfo(TDataMethodProc) = tv.TypeInfo, 'TValue should hold TMethodProc type info');
+3 -1
View File
@@ -35,7 +35,9 @@ uses
Myc.Data.Types.Void in '..\Src\Data\Myc.Data.Types.Void.pas',
Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas',
Myc.Ast in '..\Src\AST\Myc.Ast.pas',
TestAst in '..\Src\AST\TestAst.pas';
TestAst in '..\Src\AST\TestAst.pas',
Myc.Data.Types.JSON in '..\Src\Data\Myc.Data.Types.JSON.pas',
TestDataTypes.JSON in '..\Src\Data\TestDataTypes.JSON.pas';
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
{$IFNDEF TESTINSIGHT}
+2
View File
@@ -137,6 +137,8 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="..\Src\Data\Myc.Data.Types.Decimal.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.pas"/>
<DCCReference Include="..\Src\AST\TestAst.pas"/>
<DCCReference Include="..\Src\Data\Myc.Data.Types.JSON.pas"/>
<DCCReference Include="..\Src\Data\TestDataTypes.JSON.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>